feat: define validators and use normalize-products step (#12473)
Fixes: FRMW-2965 In this PR we replace/remove the existing step to normalize a CSV file with the newly written CSV normalizer and also we validate the file contents further using a Zod schema. I have duplicated the schema for now. But it is makes sense to re-use the schema for CSV validating and `/admin/products/batch`, then I can keep one source of truth under utils and re-export it. WDYT? **Screenshots of some errors after validating the file strictly**  
This commit is contained in:
7
.changeset/three-masks-bathe.md
Normal file
7
.changeset/three-masks-bathe.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"integration-tests-http": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: define validators and use normalize-products step
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Category 1,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
|
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Category 1,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
|
||||||
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
||||||
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRQ7WJ902X4936GEK7,Test variant,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,tag-123,tag-456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRQ7WJ902X4936GEK7,Test variant,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
||||||
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
||||||
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRAYY7Q7NTXNNMDA1S,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,tag-123,tag-456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRAYY7Q7NTXNNMDA1S,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
|
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
|
||||||
prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description
|
prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description
|
||||||
test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW1T9KQB6XG7Q6K61F,Test variant,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,,,import-shipping-profile
|
test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,tag-123,tag-456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW1T9KQB6XG7Q6K61F,Test variant,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,,,import-shipping-profile
|
||||||
prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description
|
prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description
|
||||||
test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW5GNQKT1FEDACEESW,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,,,import-shipping-profile
|
test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,tag-123,tag-456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW5GNQKT1FEDACEESW,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,,,import-shipping-profile
|
||||||
prod_01J44RRK2GJJVMQQXT67TJCV08,proposed-product,Proposed product,proposed,test-product-description,,,test-image.png,,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.213Z,,true,,,test-image.png,test-image-2.png,false,,,,,new-tag,,2024-07-31T16:07:55.213Z,,,variant_01J44RRK2WYHH0RDEK8BBGP7CY,Test variant,,,,,,true,false,,2024-07-31T16:07:55.228Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRK2GJJVMQQXT67TJCV08,2024-07-31T16:07:55.228Z,0,,,import-shipping-profile
|
,proposed-product,Proposed product,proposed,test-product-description,,,test-image.png,,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.213Z,,true,,,test-image.png,test-image-2.png,false,,,,,new-tag,,2024-07-31T16:07:55.213Z,,,,Test variant,,,,,,true,false,,2024-07-31T16:07:55.228Z,,,,,,size,large,color,green,,30,45,100,,2024-07-31T16:07:55.228Z,0,,,import-shipping-profile
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
Product Id;Product Handle;Product Title;Product Status;Product Description;Product Subtitle;Product External Id;Product Thumbnail;Product Collection Id;Product Type Id;Product Created At;Product Deleted At;Product Discountable;Product Height;Product Hs Code;Product Image 1;Product Image 2;Product Is Giftcard;Product Length;Product Material;Product Mid Code;Product Origin Country;Product Tag 1;Product Tag 2;Product Updated At;Product Weight;Product Width;Variant Id;Variant Title;Variant Sku;Variant Upc;Variant Ean;Variant Hs Code;Variant Mid Code;Variant Manage Inventory;Variant Allow Backorder;Variant Barcode;Variant Created At;Variant Deleted At;Variant Height;Variant Length;Variant Material;Variant Metadata;Variant Option 1 Name;Variant Option 1 Value;Variant Option 2 Name;Variant Option 2 Value;Variant Origin Country;Variant Price DKK;Variant Price EUR;Variant Price USD;Variant Product Id;Variant Updated At;Variant Variant Rank;Variant Weight;Variant Width;Shipping Profile Id
|
Product Id;Product Handle;Product Title;Product Status;Product Description;Product Subtitle;Product External Id;Product Thumbnail;Product Collection Id;Product Type Id;Product Created At;Product Deleted At;Product Discountable;Product Height;Product Hs Code;Product Image 1;Product Image 2;Product Is Giftcard;Product Length;Product Material;Product Mid Code;Product Origin Country;Product Tag 1;Product Tag 2;Product Updated At;Product Weight;Product Width;Variant Id;Variant Title;Variant Sku;Variant Upc;Variant Ean;Variant Hs Code;Variant Mid Code;Variant Manage Inventory;Variant Allow Backorder;Variant Barcode;Variant Created At;Variant Deleted At;Variant Height;Variant Length;Variant Material;Variant Metadata;Variant Option 1 Name;Variant Option 1 Value;Variant Option 2 Name;Variant Option 2 Value;Variant Origin Country;Variant Price DKK;Variant Price EUR;Variant Price USD;Variant Product Id;Variant Updated At;Variant Variant Rank;Variant Weight;Variant Width;Shipping Profile Id
|
||||||
prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description
|
prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description
|
||||||
test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;123;456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW1T9KQB6XG7Q6K61F;Test variant;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;;;import-shipping-profile
|
test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;tag-123;tag-456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW1T9KQB6XG7Q6K61F;Test variant;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;;;import-shipping-profile
|
||||||
prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description
|
prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description
|
||||||
test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;123;456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW5GNQKT1FEDACEESW;Test variant 2;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;small;color;green;;50;65;200;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;;;import-shipping-profile
|
test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;tag-123;tag-456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW5GNQKT1FEDACEESW;Test variant 2;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;small;color;green;;50;65;200;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;;;import-shipping-profile
|
||||||
prod_01J44RRK2GJJVMQQXT67TJCV08;proposed-product;Proposed product;proposed;test-product-description;;;test-image.png;;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.213Z;;true;;;test-image.png;test-image-2.png;false;;;;;new-tag;;2024-07-31T16:07:55.213Z;;;variant_01J44RRK2WYHH0RDEK8BBGP7CY;Test variant;;;;;;true;false;;2024-07-31T16:07:55.228Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRK2GJJVMQQXT67TJCV08;2024-07-31T16:07:55.228Z;0;;;import-shipping-profile
|
;proposed-product;Proposed product;proposed;test-product-description;;;test-image.png;;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.213Z;;true;;;test-image.png;test-image-2.png;false;;;;;new-tag;;2024-07-31T16:07:55.213Z;;;;Test variant;;;;;;true;false;;2024-07-31T16:07:55.228Z;;;;;;size;large;color;green;;30;45;100;;2024-07-31T16:07:55.228Z;0;;;import-shipping-profile
|
||||||
|
@@ -0,0 +1,431 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[` POST /admin/products/export should export a csv file containing the expected products 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description
|
||||||
|
test line 2",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "base-product",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "draft",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "tag-123",
|
||||||
|
"Product Tag 2": "tag-456",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Base product",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "large",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price DKK": 30,
|
||||||
|
"Variant Price EUR": 45,
|
||||||
|
"Variant Price USD": 100,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description
|
||||||
|
test line 2",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "base-product",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "draft",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "tag-123",
|
||||||
|
"Product Tag 2": "tag-456",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Base product",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "small",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price DKK": 50,
|
||||||
|
"Variant Price EUR": 65,
|
||||||
|
"Variant Price USD": 200,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant 2",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "proposed-product",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "proposed",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "new-tag",
|
||||||
|
"Product Tag 2": "",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Proposed product",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "large",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price DKK": 30,
|
||||||
|
"Variant Price EUR": 45,
|
||||||
|
"Variant Price USD": 100,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[` POST /admin/products/export should export a csv file filtered by specific products 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "proposed-product",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "proposed",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "new-tag",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Proposed product",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "large",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price DKK": 30,
|
||||||
|
"Variant Price EUR": 45,
|
||||||
|
"Variant Price USD": 100,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[` POST /admin/products/export should export a csv file with categories 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Category 1": "<ID>",
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description
|
||||||
|
test line 2",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "base-product",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "draft",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "tag-123",
|
||||||
|
"Product Tag 2": "tag-456",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Base product",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "large",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price DKK": 30,
|
||||||
|
"Variant Price EUR": 45,
|
||||||
|
"Variant Price USD": 100,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Category 1": "<ID>",
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description
|
||||||
|
test line 2",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "base-product",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "draft",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "tag-123",
|
||||||
|
"Product Tag 2": "tag-456",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Base product",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "small",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price DKK": 50,
|
||||||
|
"Variant Price EUR": 65,
|
||||||
|
"Variant Price USD": 200,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant 2",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[` POST /admin/products/export should export a csv file with region prices 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Collection Id": "<ID>",
|
||||||
|
"Product Created At": "<DateTime>",
|
||||||
|
"Product Deleted At": "<DateTime>",
|
||||||
|
"Product Description": "test-product-description",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product External Id": "<ID>",
|
||||||
|
"Product Handle": "product-with-prices",
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Id": "<ID>",
|
||||||
|
"Product Image 1": "test-image.png",
|
||||||
|
"Product Image 2": "test-image-2.png",
|
||||||
|
"Product Is Giftcard": false,
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Status": "draft",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product Tag 1": "tag-123",
|
||||||
|
"Product Tag 2": "tag-456",
|
||||||
|
"Product Thumbnail": "test-image.png",
|
||||||
|
"Product Title": "Product with prices",
|
||||||
|
"Product Type Id": "<ID>",
|
||||||
|
"Product Updated At": "<DateTime>",
|
||||||
|
"Product Weight": "",
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Created At": "<DateTime>",
|
||||||
|
"Variant Deleted At": "<DateTime>",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Id": "<ID>",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Option 1 Name": "size",
|
||||||
|
"Variant Option 1 Value": "large",
|
||||||
|
"Variant Option 2 Name": "color",
|
||||||
|
"Variant Option 2 Value": "green",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price Test Region [USD]": 45,
|
||||||
|
"Variant Price USD": 100,
|
||||||
|
"Variant Product Id": "<ID>",
|
||||||
|
"Variant Sku": "",
|
||||||
|
"Variant Title": "Test variant",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Updated At": "<DateTime>",
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
@@ -11,38 +11,103 @@ import {
|
|||||||
createAdminUser,
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||||
|
import { csv2json } from "json-2-csv"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
const compareCSVs = async (filePath, expectedFilePath) => {
|
const EXPORTED_COLUMNS = [
|
||||||
|
"Product Collection Id",
|
||||||
|
"Product Created At",
|
||||||
|
"Product Deleted At",
|
||||||
|
"Product Description",
|
||||||
|
"Product Discountable",
|
||||||
|
"Product External Id",
|
||||||
|
"Product Handle",
|
||||||
|
"Product Height",
|
||||||
|
"Product Hs Code",
|
||||||
|
"Product Id",
|
||||||
|
"Product Image *",
|
||||||
|
"Product Is Giftcard",
|
||||||
|
"Product Length",
|
||||||
|
"Product Material",
|
||||||
|
"Product Mid Code",
|
||||||
|
"Product Origin Country",
|
||||||
|
"Product Status",
|
||||||
|
"Product Subtitle",
|
||||||
|
"Product Tag *",
|
||||||
|
"Product Thumbnail",
|
||||||
|
"Product Title",
|
||||||
|
"Product Type Id",
|
||||||
|
"Product Updated At",
|
||||||
|
"Product Weight",
|
||||||
|
"Product Width",
|
||||||
|
"Variant Allow Backorder",
|
||||||
|
"Variant Barcode",
|
||||||
|
"Variant Created At",
|
||||||
|
"Variant Deleted At",
|
||||||
|
"Variant Ean",
|
||||||
|
"Variant Height",
|
||||||
|
"Variant Hs Code",
|
||||||
|
"Variant Id",
|
||||||
|
"Variant Length",
|
||||||
|
"Variant Manage Inventory",
|
||||||
|
"Variant Material",
|
||||||
|
"Variant Metadata",
|
||||||
|
"Variant Mid Code",
|
||||||
|
"Variant Option * Name",
|
||||||
|
"Variant Option * Value",
|
||||||
|
"Variant Origin Country",
|
||||||
|
"Variant Price [ISO]",
|
||||||
|
"Variant Product Id",
|
||||||
|
"Variant Sku",
|
||||||
|
"Variant Title",
|
||||||
|
"Variant Upc",
|
||||||
|
"Variant Updated At",
|
||||||
|
"Variant Variant Rank",
|
||||||
|
"Variant Weight",
|
||||||
|
"Variant Width",
|
||||||
|
]
|
||||||
|
|
||||||
|
const getCSVContents = async (filePath: string) => {
|
||||||
const asLocalPath = filePath.replace("http://localhost:9000", process.cwd())
|
const asLocalPath = filePath.replace("http://localhost:9000", process.cwd())
|
||||||
let fileContent = await fs.readFile(asLocalPath, { encoding: "utf-8" })
|
const fileContent = await fs.readFile(asLocalPath, { encoding: "utf-8" })
|
||||||
let fixturesContent = await fs.readFile(expectedFilePath, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
})
|
|
||||||
await fs.rm(path.dirname(asLocalPath), { recursive: true, force: true })
|
await fs.rm(path.dirname(asLocalPath), { recursive: true, force: true })
|
||||||
|
const csvRows = csv2json(fileContent)
|
||||||
|
|
||||||
// Normalize csv data to get rid of dynamic data
|
return csvRows.reduce<any[]>((result, row) => {
|
||||||
const idsToReplace = ["prod_", "pcol_", "variant_", "ptyp_", "pcat_"]
|
const rowCopy = { ...row }
|
||||||
const dateRegex =
|
Object.keys(rowCopy).forEach((col) => {
|
||||||
/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})\.(\d{3})Z/g
|
if (
|
||||||
idsToReplace.forEach((prefix) => {
|
col.includes("Updated At") ||
|
||||||
fileContent = fileContent.replace(
|
col.includes("Created At") ||
|
||||||
new RegExp(`${prefix}\\w*\\d*`, "g"),
|
col.includes("Deleted At")
|
||||||
"<ID>"
|
) {
|
||||||
)
|
rowCopy[col] = "<DateTime>"
|
||||||
fixturesContent = fixturesContent.replace(
|
}
|
||||||
new RegExp(`${prefix}\\w*\\d*`, "g"),
|
if (col.includes("Id") || col.startsWith("Product Category ")) {
|
||||||
"<ID>"
|
rowCopy[col] = "<ID>"
|
||||||
)
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
result.push(rowCopy)
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertExportedColumns = (rows: any[]) => {
|
||||||
|
rows.forEach((row) => {
|
||||||
|
EXPORTED_COLUMNS.forEach((column) => {
|
||||||
|
if (column.includes("[ISO]")) {
|
||||||
|
expect(
|
||||||
|
Object.keys(row).filter((rowCol) =>
|
||||||
|
rowCol.startsWith("Variant Price ")
|
||||||
|
).length
|
||||||
|
).toBeGreaterThanOrEqual(1)
|
||||||
|
} else {
|
||||||
|
expect(row).toHaveProperty(column.replace("*", "1"))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
fileContent = fileContent.replace(dateRegex, "<DATE>")
|
|
||||||
fixturesContent = fixturesContent.replace(dateRegex, "<DATE>")
|
|
||||||
|
|
||||||
fixturesContent = fixturesContent.replace(/,Shipping Profile Id*/g, "")
|
|
||||||
fixturesContent = fixturesContent.replace(/,import-shipping-profile*/g, "")
|
|
||||||
|
|
||||||
expect(fileContent).toEqual(fixturesContent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
@@ -121,11 +186,19 @@ medusaIntegrationTestRunner({
|
|||||||
).data.product_category
|
).data.product_category
|
||||||
|
|
||||||
baseTag1 = (
|
baseTag1 = (
|
||||||
await api.post("/admin/product-tags", { value: "123" }, adminHeaders)
|
await api.post(
|
||||||
|
"/admin/product-tags",
|
||||||
|
{ value: "tag-123" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.product_tag
|
).data.product_tag
|
||||||
|
|
||||||
baseTag2 = (
|
baseTag2 = (
|
||||||
await api.post("/admin/product-tags", { value: "456" }, adminHeaders)
|
await api.post(
|
||||||
|
"/admin/product-tags",
|
||||||
|
{ value: "tag-456" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.product_tag
|
).data.product_tag
|
||||||
|
|
||||||
newTag = (
|
newTag = (
|
||||||
@@ -252,10 +325,12 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await compareCSVs(
|
const exportedFileContents = await getCSVContents(
|
||||||
notifications[0].data.file.url,
|
notifications[0].data.file.url
|
||||||
path.join(__dirname, "__fixtures__", "exported-products-comma.csv")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertExportedColumns(exportedFileContents)
|
||||||
|
expect(exportedFileContents).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should export a csv file with categories", async () => {
|
it("should export a csv file with categories", async () => {
|
||||||
@@ -278,10 +353,12 @@ medusaIntegrationTestRunner({
|
|||||||
await api.get("/admin/notifications", adminHeaders)
|
await api.get("/admin/notifications", adminHeaders)
|
||||||
).data.notifications
|
).data.notifications
|
||||||
|
|
||||||
await compareCSVs(
|
const exportedFileContents = await getCSVContents(
|
||||||
notifications[0].data.file.url,
|
notifications[0].data.file.url
|
||||||
path.join(__dirname, "__fixtures__", "product-with-categories.csv")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertExportedColumns(exportedFileContents)
|
||||||
|
expect(exportedFileContents).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should export a csv file with region prices", async () => {
|
it("should export a csv file with region prices", async () => {
|
||||||
@@ -338,10 +415,12 @@ medusaIntegrationTestRunner({
|
|||||||
await api.get("/admin/notifications", adminHeaders)
|
await api.get("/admin/notifications", adminHeaders)
|
||||||
).data.notifications
|
).data.notifications
|
||||||
|
|
||||||
await compareCSVs(
|
const exportedFileContents = await getCSVContents(
|
||||||
notifications[0].data.file.url,
|
notifications[0].data.file.url
|
||||||
path.join(__dirname, "__fixtures__", "prices-with-region.csv")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertExportedColumns(exportedFileContents)
|
||||||
|
expect(exportedFileContents).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should export a csv file filtered by specific products", async () => {
|
it("should export a csv file filtered by specific products", async () => {
|
||||||
@@ -367,10 +446,12 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
expect(notifications.length).toBe(1)
|
expect(notifications.length).toBe(1)
|
||||||
|
|
||||||
await compareCSVs(
|
const exportedFileContents = await getCSVContents(
|
||||||
notifications[0].data.file.url,
|
notifications[0].data.file.url
|
||||||
path.join(__dirname, "__fixtures__", "filtered-products.csv")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertExportedColumns(exportedFileContents)
|
||||||
|
expect(exportedFileContents).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { csv2json, json2csv } from "json-2-csv"
|
||||||
import {
|
import {
|
||||||
medusaIntegrationTestRunner,
|
medusaIntegrationTestRunner,
|
||||||
TestEventUtils,
|
TestEventUtils,
|
||||||
@@ -13,6 +14,17 @@ import {
|
|||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||||
|
|
||||||
|
const UNALLOWED_EXPORTED_COLUMNS = [
|
||||||
|
"Product Is Giftcard",
|
||||||
|
"Product Created At",
|
||||||
|
"Product Updated At",
|
||||||
|
"Product Deleted At",
|
||||||
|
"Variant Product Id",
|
||||||
|
"Variant Created At",
|
||||||
|
"Variant Updated At",
|
||||||
|
"Variant Deleted At",
|
||||||
|
]
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
const getUploadReq = (file: { name: string; content: string }) => {
|
const getUploadReq = (file: { name: string; content: string }) => {
|
||||||
@@ -29,7 +41,21 @@ const getUploadReq = (file: { name: string; content: string }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareCSVForImport(fileContents: string, delimiter: string = ",") {
|
||||||
|
const CSVFileAsJSON = csv2json(fileContents, {
|
||||||
|
delimiter: { field: delimiter },
|
||||||
|
})
|
||||||
|
CSVFileAsJSON.forEach((row) => {
|
||||||
|
UNALLOWED_EXPORTED_COLUMNS.forEach((col) => {
|
||||||
|
delete row[col]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return json2csv(CSVFileAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
|
dbName: "bulk-uploads-local",
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let baseCollection
|
let baseCollection
|
||||||
let baseType
|
let baseType
|
||||||
@@ -66,15 +92,27 @@ medusaIntegrationTestRunner({
|
|||||||
).data.product_type
|
).data.product_type
|
||||||
|
|
||||||
baseTag1 = (
|
baseTag1 = (
|
||||||
await api.post("/admin/product-tags", { value: "123" }, adminHeaders)
|
await api.post(
|
||||||
|
"/admin/product-tags",
|
||||||
|
{ value: "tag-123" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.product_tag
|
).data.product_tag
|
||||||
|
|
||||||
baseTag2 = (
|
baseTag2 = (
|
||||||
await api.post("/admin/product-tags", { value: "123_1" }, adminHeaders)
|
await api.post(
|
||||||
|
"/admin/product-tags",
|
||||||
|
{ value: "tag-123_1" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.product_tag
|
).data.product_tag
|
||||||
|
|
||||||
baseTag3 = (
|
baseTag3 = (
|
||||||
await api.post("/admin/product-tags", { value: "456" }, adminHeaders)
|
await api.post(
|
||||||
|
"/admin/product-tags",
|
||||||
|
{ value: "tag-456" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.product_tag
|
).data.product_tag
|
||||||
|
|
||||||
newTag = (
|
newTag = (
|
||||||
@@ -129,13 +167,18 @@ medusaIntegrationTestRunner({
|
|||||||
;(eventBus as any).eventEmitter_.removeAllListeners()
|
;(eventBus as any).eventEmitter_.removeAllListeners()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/products/export", () => {
|
describe("POST /admin/products/import", () => {
|
||||||
// We want to ensure files with different delimiters are supported
|
// We want to ensure files with different delimiters are supported
|
||||||
;[
|
;[
|
||||||
{ file: "exported-products-comma.csv", name: "delimited with comma" },
|
|
||||||
{
|
{
|
||||||
file: "exported-products-semicolon.csv",
|
file: "products-comma.csv",
|
||||||
|
name: "delimited with comma",
|
||||||
|
delimiter: ",",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "products-semicolon.csv",
|
||||||
name: "delimited with semicolon",
|
name: "delimited with semicolon",
|
||||||
|
delimiter: ";",
|
||||||
},
|
},
|
||||||
].forEach((testcase) => {
|
].forEach((testcase) => {
|
||||||
it(`should import a previously exported products CSV file ${testcase.name}`, async () => {
|
it(`should import a previously exported products CSV file ${testcase.name}`, async () => {
|
||||||
@@ -157,8 +200,12 @@ medusaIntegrationTestRunner({
|
|||||||
/variant_01J44RRJZW1T9KQB6XG7Q6K61F/g,
|
/variant_01J44RRJZW1T9KQB6XG7Q6K61F/g,
|
||||||
baseProduct.variants[0].id
|
baseProduct.variants[0].id
|
||||||
)
|
)
|
||||||
|
|
||||||
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
|
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
|
||||||
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
|
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
|
||||||
|
fileContent = fileContent.replace(/tag-123/g, baseTag1.id)
|
||||||
|
fileContent = fileContent.replace(/tag-456/g, baseTag3.id)
|
||||||
|
fileContent = fileContent.replace(/new-tag/g, newTag.id)
|
||||||
|
|
||||||
fileContent = fileContent.replace(
|
fileContent = fileContent.replace(
|
||||||
/import-shipping-profile*/g,
|
/import-shipping-profile*/g,
|
||||||
@@ -167,7 +214,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
const { form, meta } = getUploadReq({
|
||||||
name: "test.csv",
|
name: "test.csv",
|
||||||
content: fileContent,
|
content: prepareCSVForImport(fileContent, testcase.delimiter),
|
||||||
})
|
})
|
||||||
|
|
||||||
// BREAKING: The batch endpoints moved to the domain routes (admin/batch-jobs -> /admin/products/import). The payload and response changed as well.
|
// BREAKING: The batch endpoints moved to the domain routes (admin/batch-jobs -> /admin/products/import). The payload and response changed as well.
|
||||||
@@ -209,192 +256,193 @@ medusaIntegrationTestRunner({
|
|||||||
.data.products
|
.data.products
|
||||||
|
|
||||||
expect(dbProducts).toHaveLength(2)
|
expect(dbProducts).toHaveLength(2)
|
||||||
expect(dbProducts).toEqual(
|
expect(dbProducts[0]).toEqual(
|
||||||
expect.arrayContaining([
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
id: baseProduct.id,
|
||||||
id: baseProduct.id,
|
handle: "base-product",
|
||||||
handle: "base-product",
|
is_giftcard: false,
|
||||||
is_giftcard: false,
|
thumbnail: "test-image.png",
|
||||||
thumbnail: "test-image.png",
|
status: "draft",
|
||||||
status: "draft",
|
description: "test-product-description\ntest line 2",
|
||||||
description: "test-product-description\ntest line 2",
|
options: expect.arrayContaining([
|
||||||
options: expect.arrayContaining([
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
title: "size",
|
||||||
title: "size",
|
values: expect.arrayContaining([
|
||||||
values: expect.arrayContaining([
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
value: "large",
|
||||||
value: "small",
|
}),
|
||||||
}),
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
value: "small",
|
||||||
value: "large",
|
}),
|
||||||
}),
|
]),
|
||||||
]),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "color",
|
|
||||||
values: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "green",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
images: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
url: "test-image.png",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
url: "test-image-2.png",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
tags: [
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "123",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "456",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
type: expect.objectContaining({
|
|
||||||
id: baseType.id,
|
|
||||||
}),
|
}),
|
||||||
collection: expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: baseCollection.id,
|
title: "color",
|
||||||
|
values: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "green",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
variants: expect.arrayContaining([
|
]),
|
||||||
expect.objectContaining({
|
images: expect.arrayContaining([
|
||||||
title: "Test variant",
|
expect.objectContaining({
|
||||||
allow_backorder: false,
|
url: "test-image.png",
|
||||||
manage_inventory: true,
|
}),
|
||||||
prices: expect.arrayContaining([
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
url: "test-image-2.png",
|
||||||
currency_code: "dkk",
|
}),
|
||||||
amount: 30,
|
]),
|
||||||
}),
|
tags: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "eur",
|
id: baseTag1.id,
|
||||||
amount: 45,
|
}),
|
||||||
}),
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
id: baseTag3.id,
|
||||||
currency_code: "usd",
|
}),
|
||||||
amount: 100,
|
],
|
||||||
}),
|
type: expect.objectContaining({
|
||||||
]),
|
id: baseType.id,
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "large",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "green",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "Test variant 2",
|
|
||||||
allow_backorder: false,
|
|
||||||
manage_inventory: true,
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 50,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 65,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 200,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "small",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "green",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
collection: expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: baseCollection.id,
|
||||||
handle: "proposed-product",
|
|
||||||
is_giftcard: false,
|
|
||||||
thumbnail: "test-image.png",
|
|
||||||
status: "proposed",
|
|
||||||
description: "test-product-description",
|
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "size",
|
|
||||||
values: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "large",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "color",
|
|
||||||
values: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "green",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
images: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
url: "test-image.png",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
url: "test-image-2.png",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
tags: [
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "new-tag",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
type: expect.objectContaining({
|
|
||||||
id: baseType.id,
|
|
||||||
}),
|
|
||||||
collection: null,
|
|
||||||
variants: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "Test variant",
|
|
||||||
allow_backorder: false,
|
|
||||||
manage_inventory: true,
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 30,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 45,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "large",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "green",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
])
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test variant",
|
||||||
|
allow_backorder: false,
|
||||||
|
manage_inventory: true,
|
||||||
|
prices: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 30,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 45,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
options: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "large",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "green",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test variant 2",
|
||||||
|
allow_backorder: false,
|
||||||
|
manage_inventory: true,
|
||||||
|
prices: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 50,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 65,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 200,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
options: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "small",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "green",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(dbProducts[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
handle: "proposed-product",
|
||||||
|
is_giftcard: false,
|
||||||
|
thumbnail: "test-image.png",
|
||||||
|
status: "proposed",
|
||||||
|
description: "test-product-description",
|
||||||
|
options: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "size",
|
||||||
|
values: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "large",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "color",
|
||||||
|
values: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "green",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
images: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
url: "test-image.png",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
url: "test-image-2.png",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
tags: [
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "new-tag",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
type: expect.objectContaining({
|
||||||
|
id: baseType.id,
|
||||||
|
}),
|
||||||
|
collection: null,
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test variant",
|
||||||
|
allow_backorder: false,
|
||||||
|
manage_inventory: true,
|
||||||
|
prices: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 30,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 45,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
options: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "large",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "green",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -414,6 +462,9 @@ medusaIntegrationTestRunner({
|
|||||||
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
|
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
|
||||||
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
|
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
|
||||||
fileContent = fileContent.replace(/pcat_\w*\d*/g, baseCategory.id)
|
fileContent = fileContent.replace(/pcat_\w*\d*/g, baseCategory.id)
|
||||||
|
fileContent = fileContent.replace(/tag-123/g, baseTag1.id)
|
||||||
|
fileContent = fileContent.replace(/tag-456/g, baseTag3.id)
|
||||||
|
fileContent = fileContent.replace(/new-tag/g, newTag.id)
|
||||||
|
|
||||||
fileContent = fileContent.replace(
|
fileContent = fileContent.replace(
|
||||||
/import-shipping-profile*/g,
|
/import-shipping-profile*/g,
|
||||||
@@ -422,7 +473,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
const { form, meta } = getUploadReq({
|
||||||
name: "test.csv",
|
name: "test.csv",
|
||||||
content: fileContent,
|
content: prepareCSVForImport(fileContent),
|
||||||
})
|
})
|
||||||
|
|
||||||
const batchJobRes = await api.post("/admin/products/import", form, meta)
|
const batchJobRes = await api.post("/admin/products/import", form, meta)
|
||||||
@@ -454,36 +505,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail on invalid region in prices being present in the CSV", async () => {
|
it("should complain about non-existent fields being present in the CSV", async () => {
|
||||||
let fileContent = await fs.readFile(
|
|
||||||
path.join(__dirname, "__fixtures__", "invalid-prices.csv"),
|
|
||||||
{ encoding: "utf-8" }
|
|
||||||
)
|
|
||||||
|
|
||||||
fileContent = fileContent.replace(
|
|
||||||
/import-shipping-profile*/g,
|
|
||||||
shippingProfile.id
|
|
||||||
)
|
|
||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
|
||||||
name: "test.csv",
|
|
||||||
content: fileContent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const err = await api
|
|
||||||
.post("/admin/products/import", form, meta)
|
|
||||||
.catch((e) => e)
|
|
||||||
expect(err.response.data.message).toEqual(
|
|
||||||
"Region with name nonexistent not found"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should ignore non-existent fields being present in the CSV that don't start with Product or Variant", async () => {
|
|
||||||
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
|
||||||
`${Modules.NOTIFICATION}.notification.${CommonEvents.CREATED}`,
|
|
||||||
eventBus
|
|
||||||
)
|
|
||||||
|
|
||||||
let fileContent = await fs.readFile(
|
let fileContent = await fs.readFile(
|
||||||
path.join(__dirname, "__fixtures__", "unrelated-column.csv"),
|
path.join(__dirname, "__fixtures__", "unrelated-column.csv"),
|
||||||
{ encoding: "utf-8" }
|
{ encoding: "utf-8" }
|
||||||
@@ -491,6 +513,9 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
|
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
|
||||||
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
|
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
|
||||||
|
fileContent = fileContent.replace(/tag-123/g, baseTag1.id)
|
||||||
|
fileContent = fileContent.replace(/tag-456/g, baseTag3.id)
|
||||||
|
fileContent = fileContent.replace(/new-tag/g, newTag.id)
|
||||||
|
|
||||||
fileContent = fileContent.replace(
|
fileContent = fileContent.replace(
|
||||||
/import-shipping-profile*/g,
|
/import-shipping-profile*/g,
|
||||||
@@ -499,46 +524,19 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
const { form, meta } = getUploadReq({
|
||||||
name: "test.csv",
|
name: "test.csv",
|
||||||
content: fileContent,
|
content: prepareCSVForImport(fileContent),
|
||||||
})
|
})
|
||||||
|
|
||||||
const batchJobRes = await api.post("/admin/products/import", form, meta)
|
const batchJobRes = await api
|
||||||
|
.post("/admin/products/import", form, meta)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
const transactionId = batchJobRes.data.transaction_id
|
expect(batchJobRes.response.data.message).toEqual(
|
||||||
expect(transactionId).toBeTruthy()
|
'Invalid column name(s) "Some field"'
|
||||||
expect(batchJobRes.data.summary).toEqual({
|
|
||||||
toCreate: 1,
|
|
||||||
toUpdate: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/import/${transactionId}/confirm`,
|
|
||||||
{},
|
|
||||||
meta
|
|
||||||
)
|
|
||||||
|
|
||||||
await subscriberExecution
|
|
||||||
const notifications = (
|
|
||||||
await api.get("/admin/notifications", adminHeaders)
|
|
||||||
).data.notifications
|
|
||||||
|
|
||||||
expect(notifications.length).toBe(1)
|
|
||||||
expect(notifications[0]).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: expect.objectContaining({
|
|
||||||
title: "Product import",
|
|
||||||
description: `Product import of file test.csv completed successfully!`,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should successfully skip non-existent product fields being present in the CSV", async () => {
|
it("should successfully skip non-existent product fields being present in the CSV", async () => {
|
||||||
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
|
||||||
`${Modules.NOTIFICATION}.notification.${CommonEvents.CREATED}`,
|
|
||||||
eventBus
|
|
||||||
)
|
|
||||||
|
|
||||||
let fileContent = await fs.readFile(
|
let fileContent = await fs.readFile(
|
||||||
path.join(__dirname, "__fixtures__", "invalid-column.csv"),
|
path.join(__dirname, "__fixtures__", "invalid-column.csv"),
|
||||||
{ encoding: "utf-8" }
|
{ encoding: "utf-8" }
|
||||||
@@ -554,313 +552,15 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
const { form, meta } = getUploadReq({
|
||||||
name: "test.csv",
|
name: "test.csv",
|
||||||
content: fileContent,
|
content: prepareCSVForImport(fileContent),
|
||||||
})
|
})
|
||||||
|
|
||||||
const batchJobRes = await api.post("/admin/products/import", form, meta)
|
const batchJobRes = await api
|
||||||
|
|
||||||
const transactionId = batchJobRes.data.transaction_id
|
|
||||||
expect(transactionId).toBeTruthy()
|
|
||||||
expect(batchJobRes.data.summary).toEqual({
|
|
||||||
toCreate: 1,
|
|
||||||
toUpdate: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/import/${transactionId}/confirm`,
|
|
||||||
{},
|
|
||||||
meta
|
|
||||||
)
|
|
||||||
|
|
||||||
await subscriberExecution
|
|
||||||
const notifications = (
|
|
||||||
await api.get("/admin/notifications", adminHeaders)
|
|
||||||
).data.notifications
|
|
||||||
|
|
||||||
expect(notifications.length).toBe(1)
|
|
||||||
expect(notifications[0]).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: expect.objectContaining({
|
|
||||||
title: "Product import",
|
|
||||||
description:
|
|
||||||
"Product import of file test.csv completed successfully!",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const [importedProduct] = (
|
|
||||||
await api.get("/admin/products?limit=1&order=-id", adminHeaders)
|
|
||||||
).data.products
|
|
||||||
|
|
||||||
expect(importedProduct).not.toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
field: "Test product",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("supports importing the v1 template", async () => {
|
|
||||||
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
|
||||||
`${Modules.NOTIFICATION}.notification.${CommonEvents.CREATED}`,
|
|
||||||
eventBus
|
|
||||||
)
|
|
||||||
|
|
||||||
let fileContent = await fs.readFile(
|
|
||||||
path.join(__dirname, "__fixtures__", "v1-template.csv"),
|
|
||||||
{ encoding: "utf-8" }
|
|
||||||
)
|
|
||||||
|
|
||||||
fileContent = fileContent.replace(
|
|
||||||
/existing-product-id/g,
|
|
||||||
baseProduct.id
|
|
||||||
)
|
|
||||||
fileContent = fileContent.replace(
|
|
||||||
/existing-variant-id/g,
|
|
||||||
baseProduct.variants[0].id
|
|
||||||
)
|
|
||||||
fileContent = fileContent.replace(/test-type/g, baseType.value)
|
|
||||||
fileContent = fileContent.replace(
|
|
||||||
/test-collection1/g,
|
|
||||||
baseCollection.handle
|
|
||||||
)
|
|
||||||
fileContent = fileContent.replace(
|
|
||||||
/test-collection2/g,
|
|
||||||
baseCollection.handle
|
|
||||||
)
|
|
||||||
|
|
||||||
fileContent = fileContent.replace(
|
|
||||||
/import-shipping-profile*/g,
|
|
||||||
shippingProfile.id
|
|
||||||
)
|
|
||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
|
||||||
name: "test.csv",
|
|
||||||
content: fileContent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const batchJobRes = await api.post("/admin/products/import", form, meta)
|
|
||||||
const transactionId = batchJobRes.data.transaction_id
|
|
||||||
expect(transactionId).toBeTruthy()
|
|
||||||
expect(batchJobRes.data.summary).toEqual({
|
|
||||||
toCreate: 2,
|
|
||||||
toUpdate: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/import/${transactionId}/confirm`,
|
|
||||||
{},
|
|
||||||
meta
|
|
||||||
)
|
|
||||||
|
|
||||||
await subscriberExecution
|
|
||||||
const dbProducts = (await api.get("/admin/products", adminHeaders)).data
|
|
||||||
.products
|
|
||||||
|
|
||||||
expect(dbProducts).toHaveLength(3)
|
|
||||||
expect(dbProducts).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: baseProduct.id,
|
|
||||||
handle: "test-product-product-2",
|
|
||||||
title: "Test product",
|
|
||||||
is_giftcard: false,
|
|
||||||
thumbnail: "test-image.png",
|
|
||||||
status: "draft",
|
|
||||||
description: "test-product-description",
|
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "Size",
|
|
||||||
values: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "Small",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "Medium",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "Large",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
images: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
url: "test-image.png",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
tags: [
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "123",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
type: expect.objectContaining({
|
|
||||||
id: baseType.id,
|
|
||||||
}),
|
|
||||||
collection: expect.objectContaining({
|
|
||||||
id: baseCollection.id,
|
|
||||||
}),
|
|
||||||
variants: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "Test variant",
|
|
||||||
sku: "test-sku-2",
|
|
||||||
barcode: "test-barcode-2",
|
|
||||||
allow_backorder: false,
|
|
||||||
manage_inventory: true,
|
|
||||||
prices: [
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 1.1,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
options: [
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "Small",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "Test variant",
|
|
||||||
sku: "test-sku-3",
|
|
||||||
barcode: "test-barcode-3",
|
|
||||||
allow_backorder: false,
|
|
||||||
manage_inventory: true,
|
|
||||||
prices: [
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 1.2,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
options: [
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "Medium",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: baseProduct.variants[0].id,
|
|
||||||
title: "Test variant changed",
|
|
||||||
sku: "test-sku-4",
|
|
||||||
barcode: "test-barcode-4",
|
|
||||||
allow_backorder: false,
|
|
||||||
manage_inventory: true,
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
}),
|
|
||||||
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 45,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 30,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
options: [
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "Large",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
title: "Test product",
|
|
||||||
handle: "test-product-product-1-1",
|
|
||||||
is_giftcard: false,
|
|
||||||
thumbnail: "test-image.png",
|
|
||||||
status: "draft",
|
|
||||||
description:
|
|
||||||
"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",
|
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "test-option-1",
|
|
||||||
values: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "option 1 value red",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "test-option-2",
|
|
||||||
values: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "option 2 value 1",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
images: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
url: "test-image.png",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
tags: [],
|
|
||||||
type: null,
|
|
||||||
variants: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "Test variant",
|
|
||||||
sku: "test-sku-1-1",
|
|
||||||
barcode: "test-barcode-1-1",
|
|
||||||
allow_backorder: false,
|
|
||||||
manage_inventory: true,
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
rules: {
|
|
||||||
region_id: baseRegion.id,
|
|
||||||
},
|
|
||||||
amount: 1,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 1.1,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
options: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "option 1 value red",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
value: "option 2 value 1",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
}),
|
|
||||||
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
title: "Test product",
|
|
||||||
handle: "test-product-product-1",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should fail when a v1 import has non existent collection", async () => {
|
|
||||||
let fileContent = await fs.readFile(
|
|
||||||
path.join(__dirname, "__fixtures__", "v1-template.csv"),
|
|
||||||
{ encoding: "utf-8" }
|
|
||||||
)
|
|
||||||
|
|
||||||
const { form, meta } = getUploadReq({
|
|
||||||
name: "test.csv",
|
|
||||||
content: fileContent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const err = await api
|
|
||||||
.post("/admin/products/import", form, meta)
|
.post("/admin/products/import", form, meta)
|
||||||
.catch((e) => e)
|
.catch((e) => e)
|
||||||
expect(err.response.data.message).toEqual(
|
|
||||||
"Product collection with handle 'test-collection1' does not exist"
|
expect(batchJobRes.response.data.message).toEqual(
|
||||||
|
'Invalid column name(s) "Product field"'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@swc/core": "^1.4.8",
|
"@swc/core": "^1.4.8",
|
||||||
"@swc/jest": "^0.2.36",
|
"@swc/jest": "^0.2.36",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
"json-2-csv": "^5.5.9",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { Modules } from "@medusajs/framework/utils"
|
import { Modules } from "@medusajs/framework/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { normalizeForExport } from "../helpers/normalize-for-export"
|
import { normalizeForExport } from "../helpers/normalize-for-export"
|
||||||
import { convertJsonToCsv } from "../utlils"
|
import { convertJsonToCsv } from "../utils"
|
||||||
|
|
||||||
const prodColumnPositions = new Map([
|
const prodColumnPositions = new Map([
|
||||||
["Product Id", 0],
|
["Product Id", 0],
|
||||||
@@ -81,13 +81,13 @@ export const generateProductCsvStepId = "generate-product-csv"
|
|||||||
/**
|
/**
|
||||||
* This step generates a CSV file that exports products. The CSV
|
* This step generates a CSV file that exports products. The CSV
|
||||||
* file is created and stored using the registered [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file).
|
* file is created and stored using the registered [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file).
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { data: products } = useQueryGraphStep({
|
* const { data: products } = useQueryGraphStep({
|
||||||
* entity: "product",
|
* entity: "product",
|
||||||
* fields: ["*", "variants.*", "collection.*", "categories.*"]
|
* fields: ["*", "variants.*", "collection.*", "categories.*"]
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* // @ts-ignore
|
* // @ts-ignore
|
||||||
* const data = generateProductCsvStep(products)
|
* const data = generateProductCsvStep(products)
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +118,7 @@ export const generateProductCsvStep = createStep(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return new StepResponse(
|
return new StepResponse(
|
||||||
{ id: file.id, filename } as GenerateProductCsvStepOutput,
|
{ id: file.id, filename } as GenerateProductCsvStepOutput,
|
||||||
file.id
|
file.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
import { HttpTypes, IProductModuleService } from "@medusajs/framework/types"
|
|
||||||
import { Modules } from "@medusajs/framework/utils"
|
|
||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The products to group.
|
|
||||||
*/
|
|
||||||
export type GroupProductsForBatchStepInput = (HttpTypes.AdminCreateProduct & {
|
|
||||||
/**
|
|
||||||
* The ID of the product to update.
|
|
||||||
*/
|
|
||||||
id?: string
|
|
||||||
})[]
|
|
||||||
|
|
||||||
export type GroupProductsForBatchStepOutput = {
|
|
||||||
/**
|
|
||||||
* The products to create.
|
|
||||||
*/
|
|
||||||
create: HttpTypes.AdminCreateProduct[]
|
|
||||||
/**
|
|
||||||
* The products to update.
|
|
||||||
*/
|
|
||||||
update: (HttpTypes.AdminUpdateProduct & { id: string })[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const groupProductsForBatchStepId = "group-products-for-batch"
|
|
||||||
/**
|
|
||||||
* This step groups products to be created or updated.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const data = groupProductsForBatchStep([
|
|
||||||
* {
|
|
||||||
* id: "prod_123",
|
|
||||||
* title: "Shirt",
|
|
||||||
* options: [
|
|
||||||
* {
|
|
||||||
* title: "Size",
|
|
||||||
* values: ["S", "M", "L"]
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* title: "Pants",
|
|
||||||
* options: [
|
|
||||||
* {
|
|
||||||
* title: "Color",
|
|
||||||
* values: ["Red", "Blue"]
|
|
||||||
* }
|
|
||||||
* ],
|
|
||||||
* variants: [
|
|
||||||
* {
|
|
||||||
* title: "Red Pants",
|
|
||||||
* options: {
|
|
||||||
* Color: "Red"
|
|
||||||
* },
|
|
||||||
* prices: [
|
|
||||||
* {
|
|
||||||
* amount: 10,
|
|
||||||
* currency_code: "usd"
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* ])
|
|
||||||
*/
|
|
||||||
export const groupProductsForBatchStep = createStep(
|
|
||||||
groupProductsForBatchStepId,
|
|
||||||
async (
|
|
||||||
data: GroupProductsForBatchStepInput,
|
|
||||||
{ container }
|
|
||||||
) => {
|
|
||||||
const service = container.resolve<IProductModuleService>(Modules.PRODUCT)
|
|
||||||
|
|
||||||
const existingProducts = await service.listProducts(
|
|
||||||
{
|
|
||||||
// We use the ID to do product updates
|
|
||||||
id: data.map((product) => product.id).filter(Boolean) as string[],
|
|
||||||
},
|
|
||||||
{ select: ["handle"] }
|
|
||||||
)
|
|
||||||
const existingProductsSet = new Set(existingProducts.map((p) => p.id))
|
|
||||||
|
|
||||||
const { toUpdate, toCreate } = data.reduce(
|
|
||||||
(
|
|
||||||
acc: {
|
|
||||||
toUpdate: (HttpTypes.AdminUpdateProduct & { id: string })[]
|
|
||||||
toCreate: HttpTypes.AdminCreateProduct[]
|
|
||||||
},
|
|
||||||
product
|
|
||||||
) => {
|
|
||||||
// There are few data normalizations to do if we are dealing with an update.
|
|
||||||
if (product.id && existingProductsSet.has(product.id)) {
|
|
||||||
acc.toUpdate.push(
|
|
||||||
product as HttpTypes.AdminUpdateProduct & { id: string }
|
|
||||||
)
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
|
|
||||||
// New products and variants will be created with a new ID, even if there is one present in the CSV.
|
|
||||||
// To add support for creating with predefined IDs we will need to do changes to the upsert method.
|
|
||||||
delete product.id
|
|
||||||
product.variants?.forEach((variant) => {
|
|
||||||
delete (variant as any).id
|
|
||||||
})
|
|
||||||
|
|
||||||
acc.toCreate.push(product)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{ toUpdate: [], toCreate: [] }
|
|
||||||
)
|
|
||||||
|
|
||||||
return new StepResponse(
|
|
||||||
{ create: toCreate, update: toUpdate } as GroupProductsForBatchStepOutput,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -23,7 +23,6 @@ export * from "./update-product-tags"
|
|||||||
export * from "./delete-product-tags"
|
export * from "./delete-product-tags"
|
||||||
export * from "./generate-product-csv"
|
export * from "./generate-product-csv"
|
||||||
export * from "./parse-product-csv"
|
export * from "./parse-product-csv"
|
||||||
export * from "./group-products-for-batch"
|
|
||||||
export * from "./wait-confirmation-product-import"
|
export * from "./wait-confirmation-product-import"
|
||||||
export * from "./get-variant-availability"
|
export * from "./get-variant-availability"
|
||||||
export * from "./normalize-products-v1"
|
export * from "./normalize-products"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CSVNormalizer } from "@medusajs/framework/utils"
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import { CSVNormalizer, productValidators } from "@medusajs/framework/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { convertCsvToJson } from "../utlils"
|
import { convertCsvToJson } from "../utils"
|
||||||
import { GroupProductsForBatchStepOutput } from "./group-products-for-batch"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CSV file content to parse.
|
* The CSV file content to parse.
|
||||||
@@ -18,7 +18,7 @@ export const normalizeCsvStepId = "normalize-product-csv"
|
|||||||
*/
|
*/
|
||||||
export const normalizeCsvStep = createStep(
|
export const normalizeCsvStep = createStep(
|
||||||
normalizeCsvStepId,
|
normalizeCsvStepId,
|
||||||
async (fileContent: NormalizeProductCsvStepInput, { container }) => {
|
async (fileContent: NormalizeProductCsvStepInput) => {
|
||||||
const csvProducts =
|
const csvProducts =
|
||||||
convertCsvToJson<ConstructorParameters<typeof CSVNormalizer>[0][0]>(
|
convertCsvToJson<ConstructorParameters<typeof CSVNormalizer>[0][0]>(
|
||||||
fileContent
|
fileContent
|
||||||
@@ -27,22 +27,28 @@ export const normalizeCsvStep = createStep(
|
|||||||
const products = normalizer.proccess()
|
const products = normalizer.proccess()
|
||||||
|
|
||||||
const create = Object.keys(products.toCreate).reduce<
|
const create = Object.keys(products.toCreate).reduce<
|
||||||
(typeof products)["toCreate"][keyof (typeof products)["toCreate"]][]
|
HttpTypes.AdminCreateProduct[]
|
||||||
>((result, toCreateHandle) => {
|
>((result, toCreateHandle) => {
|
||||||
result.push(products.toCreate[toCreateHandle])
|
result.push(
|
||||||
|
productValidators.CreateProduct.parse(
|
||||||
|
products.toCreate[toCreateHandle]
|
||||||
|
) as HttpTypes.AdminCreateProduct
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const update = Object.keys(products.toUpdate).reduce<
|
const update = Object.keys(products.toUpdate).reduce<
|
||||||
(typeof products)["toUpdate"][keyof (typeof products)["toUpdate"]][]
|
HttpTypes.AdminUpdateProduct & { id: string }[]
|
||||||
>((result, toCreateId) => {
|
>((result, toUpdateId) => {
|
||||||
result.push(products.toUpdate[toCreateId])
|
result.push(
|
||||||
|
productValidators.UpdateProduct.parse(products.toUpdate[toUpdateId])
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return new StepResponse({
|
return new StepResponse({
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
} as GroupProductsForBatchStepOutput)
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -8,7 +8,7 @@ import { MedusaError, Modules } from "@medusajs/framework/utils"
|
|||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { normalizeForImport } from "../helpers/normalize-for-import"
|
import { normalizeForImport } from "../helpers/normalize-for-import"
|
||||||
import { normalizeV1Products } from "../helpers/normalize-v1-import"
|
import { normalizeV1Products } from "../helpers/normalize-v1-import"
|
||||||
import { convertCsvToJson } from "../utlils"
|
import { convertCsvToJson } from "../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CSV file content to parse.
|
* The CSV file content to parse.
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import {
|
|||||||
transform,
|
transform,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { notifyOnFailureStep, sendNotificationsStep } from "../../notification"
|
import { notifyOnFailureStep, sendNotificationsStep } from "../../notification"
|
||||||
import {
|
import { normalizeCsvStep, waitConfirmationProductImportStep } from "../steps"
|
||||||
groupProductsForBatchStep,
|
|
||||||
parseProductCsvStep,
|
|
||||||
waitConfirmationProductImportStep,
|
|
||||||
} from "../steps"
|
|
||||||
import { batchProductsWorkflow } from "./batch-products"
|
import { batchProductsWorkflow } from "./batch-products"
|
||||||
|
|
||||||
export const importProductsWorkflowId = "import-products"
|
export const importProductsWorkflowId = "import-products"
|
||||||
@@ -94,8 +90,7 @@ export const importProductsWorkflow = createWorkflow(
|
|||||||
(
|
(
|
||||||
input: WorkflowData<WorkflowTypes.ProductWorkflow.ImportProductsDTO>
|
input: WorkflowData<WorkflowTypes.ProductWorkflow.ImportProductsDTO>
|
||||||
): WorkflowResponse<WorkflowTypes.ProductWorkflow.ImportProductsSummary> => {
|
): WorkflowResponse<WorkflowTypes.ProductWorkflow.ImportProductsSummary> => {
|
||||||
const products = parseProductCsvStep(input.fileContent)
|
const batchRequest = normalizeCsvStep(input.fileContent)
|
||||||
const batchRequest = groupProductsForBatchStep(products)
|
|
||||||
|
|
||||||
const summary = transform({ batchRequest }, (data) => {
|
const summary = transform({ batchRequest }, (data) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -78,7 +78,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"toUpdate": {},
|
"toUpdate": {},
|
||||||
@@ -136,11 +136,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -157,11 +157,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -178,11 +178,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -191,7 +191,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"toUpdate": {},
|
"toUpdate": {},
|
||||||
@@ -258,11 +258,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -280,11 +280,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -303,11 +303,11 @@ describe("CSV processor", () => {
|
|||||||
"origin_country": "EU",
|
"origin_country": "EU",
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -316,7 +316,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"toUpdate": {},
|
"toUpdate": {},
|
||||||
@@ -377,11 +377,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -398,11 +398,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -419,11 +419,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -440,11 +440,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -453,7 +453,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
"sweatpants": {
|
"sweatpants": {
|
||||||
"categories": [],
|
"categories": [],
|
||||||
@@ -498,11 +498,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -519,11 +519,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -540,11 +540,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -561,11 +561,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -574,7 +574,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
"t-shirt": {
|
"t-shirt": {
|
||||||
"categories": [],
|
"categories": [],
|
||||||
@@ -600,12 +600,8 @@ describe("CSV processor", () => {
|
|||||||
"title": "Size",
|
"title": "Size",
|
||||||
"values": [
|
"values": [
|
||||||
"S",
|
"S",
|
||||||
"S",
|
|
||||||
"M",
|
|
||||||
"M",
|
"M",
|
||||||
"L",
|
"L",
|
||||||
"L",
|
|
||||||
"XL",
|
|
||||||
"XL",
|
"XL",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -614,12 +610,6 @@ describe("CSV processor", () => {
|
|||||||
"values": [
|
"values": [
|
||||||
"Black",
|
"Black",
|
||||||
"White",
|
"White",
|
||||||
"Black",
|
|
||||||
"White",
|
|
||||||
"Black",
|
|
||||||
"White",
|
|
||||||
"Black",
|
|
||||||
"White",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -643,11 +633,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -665,11 +655,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -687,11 +677,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -709,11 +699,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -731,11 +721,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -753,11 +743,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -775,11 +765,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -797,11 +787,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -810,7 +800,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"toUpdate": {
|
"toUpdate": {
|
||||||
@@ -858,11 +848,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -879,11 +869,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -900,11 +890,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -921,11 +911,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "10",
|
"amount": 10,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "15",
|
"amount": 15,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -934,7 +924,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
"prod_01JT598HEWAE555V0A6BD602MG": {
|
"prod_01JT598HEWAE555V0A6BD602MG": {
|
||||||
"categories": [],
|
"categories": [],
|
||||||
@@ -970,11 +960,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "1000",
|
"amount": 1000,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "1200",
|
"amount": 1200,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -982,7 +972,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
"prod_01JT598HEX26EHDG7SRK37Q3FG": {
|
"prod_01JT598HEX26EHDG7SRK37Q3FG": {
|
||||||
"categories": [],
|
"categories": [],
|
||||||
@@ -1024,11 +1014,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "2950",
|
"amount": 2950,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "3350",
|
"amount": 3350,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1044,11 +1034,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "2950",
|
"amount": 2950,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "3350",
|
"amount": 3350,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1064,11 +1054,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "2950",
|
"amount": 2950,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "3350",
|
"amount": 3350,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1084,11 +1074,11 @@ describe("CSV processor", () => {
|
|||||||
},
|
},
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"amount": "2950",
|
"amount": 2950,
|
||||||
"currency_code": "eur",
|
"currency_code": "eur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": "3350",
|
"amount": 3350,
|
||||||
"currency_code": "usd",
|
"currency_code": "usd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1096,7 +1086,7 @@ describe("CSV processor", () => {
|
|||||||
"variant_rank": 0,
|
"variant_rank": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"weight": "400",
|
"weight": 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,25 +169,17 @@ const productStaticColumns: {
|
|||||||
"product discountable",
|
"product discountable",
|
||||||
"discountable"
|
"discountable"
|
||||||
),
|
),
|
||||||
"product height": processAsNumber("product height", "height", {
|
"product height": processAsNumber("product height", "height"),
|
||||||
asNumericString: true,
|
|
||||||
}),
|
|
||||||
"product hs code": processAsString("product hs code", "hs_code"),
|
"product hs code": processAsString("product hs code", "hs_code"),
|
||||||
"product length": processAsNumber("product length", "length", {
|
"product length": processAsNumber("product length", "length"),
|
||||||
asNumericString: true,
|
|
||||||
}),
|
|
||||||
"product material": processAsString("product material", "material"),
|
"product material": processAsString("product material", "material"),
|
||||||
"product mid code": processAsString("product mid code", "mid_code"),
|
"product mid code": processAsString("product mid code", "mid_code"),
|
||||||
"product origin country": processAsString(
|
"product origin country": processAsString(
|
||||||
"product origin country",
|
"product origin country",
|
||||||
"origin_country"
|
"origin_country"
|
||||||
),
|
),
|
||||||
"product weight": processAsNumber("product weight", "weight", {
|
"product weight": processAsNumber("product weight", "weight"),
|
||||||
asNumericString: true,
|
"product width": processAsNumber("product width", "width"),
|
||||||
}),
|
|
||||||
"product width": processAsNumber("product width", "width", {
|
|
||||||
asNumericString: true,
|
|
||||||
}),
|
|
||||||
"product metadata": processAsString("product metadata", "metadata"),
|
"product metadata": processAsString("product metadata", "metadata"),
|
||||||
"shipping profile id": processAsString(
|
"shipping profile id": processAsString(
|
||||||
"shipping profile id",
|
"shipping profile id",
|
||||||
@@ -243,12 +235,8 @@ const variantStaticColumns: {
|
|||||||
"allow_backorder"
|
"allow_backorder"
|
||||||
),
|
),
|
||||||
"variant barcode": processAsString("variant barcode", "barcode"),
|
"variant barcode": processAsString("variant barcode", "barcode"),
|
||||||
"variant height": processAsNumber("variant height", "height", {
|
"variant height": processAsNumber("variant height", "height"),
|
||||||
asNumericString: true,
|
"variant length": processAsNumber("variant length", "length"),
|
||||||
}),
|
|
||||||
"variant length": processAsNumber("variant length", "length", {
|
|
||||||
asNumericString: true,
|
|
||||||
}),
|
|
||||||
"variant material": processAsString("variant material", "material"),
|
"variant material": processAsString("variant material", "material"),
|
||||||
"variant metadata": processAsString("variant metadata", "metadata"),
|
"variant metadata": processAsString("variant metadata", "metadata"),
|
||||||
"variant origin country": processAsString(
|
"variant origin country": processAsString(
|
||||||
@@ -259,12 +247,8 @@ const variantStaticColumns: {
|
|||||||
"variant variant rank",
|
"variant variant rank",
|
||||||
"variant_rank"
|
"variant_rank"
|
||||||
),
|
),
|
||||||
"variant width": processAsNumber("variant width", "width", {
|
"variant width": processAsNumber("variant width", "width"),
|
||||||
asNumericString: true,
|
"variant weight": processAsNumber("variant weight", "weight"),
|
||||||
}),
|
|
||||||
"variant weight": processAsNumber("variant weight", "weight", {
|
|
||||||
asNumericString: true,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,7 +279,7 @@ const variantWildcardColumns: {
|
|||||||
} else {
|
} else {
|
||||||
output["prices"].push({
|
output["prices"].push({
|
||||||
currency_code: iso,
|
currency_code: iso,
|
||||||
amount: String(numericValue),
|
amount: numericValue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -438,7 +422,7 @@ export class CSVNormalizer {
|
|||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
if (unknownColumns.length) {
|
if (unknownColumns.length) {
|
||||||
return new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Invalid column name(s) "${unknownColumns.join('","')}"`
|
`Invalid column name(s) "${unknownColumns.join('","')}"`
|
||||||
)
|
)
|
||||||
@@ -509,7 +493,7 @@ export class CSVNormalizer {
|
|||||||
)
|
)
|
||||||
if (!matchingKey) {
|
if (!matchingKey) {
|
||||||
product.options.push({ title: key, values: [value] })
|
product.options.push({ title: key, values: [value] })
|
||||||
} else {
|
} else if (!matchingKey.values.includes(value)) {
|
||||||
matchingKey.values.push(value)
|
matchingKey.values.push(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
6
packages/core/utils/src/product/enums.ts
Normal file
6
packages/core/utils/src/product/enums.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum ProductStatus {
|
||||||
|
DRAFT = "draft",
|
||||||
|
PROPOSED = "proposed",
|
||||||
|
PUBLISHED = "published",
|
||||||
|
REJECTED = "rejected",
|
||||||
|
}
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
export enum ProductStatus {
|
|
||||||
DRAFT = "draft",
|
|
||||||
PROPOSED = "proposed",
|
|
||||||
PUBLISHED = "published",
|
|
||||||
REJECTED = "rejected",
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from "./events"
|
export * from "./events"
|
||||||
export * from "./get-variant-availability"
|
export * from "./enums"
|
||||||
export * from "./csv-normalizer"
|
export * from "./csv-normalizer"
|
||||||
|
export * from "./get-variant-availability"
|
||||||
|
export * as productValidators from "./validators"
|
||||||
|
|||||||
173
packages/core/utils/src/product/validators.ts
Normal file
173
packages/core/utils/src/product/validators.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { ProductStatus } from "./enums"
|
||||||
|
|
||||||
|
export const booleanString = () =>
|
||||||
|
z
|
||||||
|
.union([z.boolean(), z.string()])
|
||||||
|
.refine((value) => {
|
||||||
|
return ["true", "false"].includes(value.toString().toLowerCase())
|
||||||
|
})
|
||||||
|
.transform((value) => {
|
||||||
|
return value.toString().toLowerCase() === "true"
|
||||||
|
})
|
||||||
|
|
||||||
|
export const numericString = () =>
|
||||||
|
z.number().transform((value) => {
|
||||||
|
return value !== null && value !== undefined ? String(value) : value
|
||||||
|
})
|
||||||
|
|
||||||
|
export const IdAssociation = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusEnum = z.nativeEnum(ProductStatus)
|
||||||
|
|
||||||
|
export const CreateVariantPrice = z.object({
|
||||||
|
currency_code: z.string(),
|
||||||
|
amount: z.number(),
|
||||||
|
min_quantity: z.number().nullish(),
|
||||||
|
max_quantity: z.number().nullish(),
|
||||||
|
rules: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateProductOption = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
values: z.array(z.string()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateProductVariant = z
|
||||||
|
.object({
|
||||||
|
title: z.string(),
|
||||||
|
sku: z.string().nullish(),
|
||||||
|
ean: z.string().nullish(),
|
||||||
|
upc: z.string().nullish(),
|
||||||
|
barcode: z.string().nullish(),
|
||||||
|
hs_code: z.string().nullish(),
|
||||||
|
mid_code: z.string().nullish(),
|
||||||
|
allow_backorder: booleanString().optional().default(false),
|
||||||
|
manage_inventory: booleanString().optional().default(true),
|
||||||
|
variant_rank: z.number().optional(),
|
||||||
|
weight: z.number().nullish(),
|
||||||
|
length: z.number().nullish(),
|
||||||
|
height: z.number().nullish(),
|
||||||
|
width: z.number().nullish(),
|
||||||
|
origin_country: z.string().nullish(),
|
||||||
|
material: z.string().nullish(),
|
||||||
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
|
prices: z.array(CreateVariantPrice),
|
||||||
|
options: z.record(z.string()).optional(),
|
||||||
|
inventory_items: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
inventory_item_id: z.string(),
|
||||||
|
required_quantity: z.number(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export const CreateProduct = z
|
||||||
|
.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string().nullish(),
|
||||||
|
description: z.string().nullish(),
|
||||||
|
is_giftcard: booleanString().optional().default(false),
|
||||||
|
discountable: booleanString().optional().default(true),
|
||||||
|
images: z.array(z.object({ url: z.string() })).optional(),
|
||||||
|
thumbnail: z.string().nullish(),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
status: statusEnum.optional().default(ProductStatus.DRAFT),
|
||||||
|
external_id: z.string().nullish(),
|
||||||
|
type_id: z.string().nullish(),
|
||||||
|
collection_id: z.string().nullish(),
|
||||||
|
categories: z.array(IdAssociation).optional(),
|
||||||
|
tags: z.array(IdAssociation).optional(),
|
||||||
|
options: z.array(CreateProductOption).optional(),
|
||||||
|
variants: z.array(CreateProductVariant).optional(),
|
||||||
|
sales_channels: z.array(z.object({ id: z.string() })).optional(),
|
||||||
|
shipping_profile_id: z.string().optional(),
|
||||||
|
weight: z.number().nullish(),
|
||||||
|
length: z.number().nullish(),
|
||||||
|
height: z.number().nullish(),
|
||||||
|
width: z.number().nullish(),
|
||||||
|
hs_code: z.string().nullish(),
|
||||||
|
mid_code: z.string().nullish(),
|
||||||
|
origin_country: z.string().nullish(),
|
||||||
|
material: z.string().nullish(),
|
||||||
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export const UpdateProductOption = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
values: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UpdateVariantPrice = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
currency_code: z.string().optional(),
|
||||||
|
amount: z.number().optional(),
|
||||||
|
min_quantity: z.number().nullish(),
|
||||||
|
max_quantity: z.number().nullish(),
|
||||||
|
rules: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UpdateProductVariant = z
|
||||||
|
.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
prices: z.array(UpdateVariantPrice).optional(),
|
||||||
|
sku: z.string().nullish(),
|
||||||
|
ean: z.string().nullish(),
|
||||||
|
upc: z.string().nullish(),
|
||||||
|
barcode: z.string().nullish(),
|
||||||
|
hs_code: z.string().nullish(),
|
||||||
|
mid_code: z.string().nullish(),
|
||||||
|
allow_backorder: booleanString().optional(),
|
||||||
|
manage_inventory: booleanString().optional(),
|
||||||
|
variant_rank: z.number().optional(),
|
||||||
|
weight: numericString().nullish(),
|
||||||
|
length: numericString().nullish(),
|
||||||
|
height: numericString().nullish(),
|
||||||
|
width: numericString().nullish(),
|
||||||
|
origin_country: z.string().nullish(),
|
||||||
|
material: z.string().nullish(),
|
||||||
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
|
options: z.record(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export const UpdateProduct = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
discountable: booleanString().optional(),
|
||||||
|
is_giftcard: booleanString().optional(),
|
||||||
|
options: z.array(UpdateProductOption).optional(),
|
||||||
|
variants: z.array(UpdateProductVariant).optional(),
|
||||||
|
status: statusEnum.optional(),
|
||||||
|
subtitle: z.string().nullish(),
|
||||||
|
description: z.string().nullish(),
|
||||||
|
images: z.array(z.object({ url: z.string() })).optional(),
|
||||||
|
thumbnail: z.string().nullish(),
|
||||||
|
handle: z.string().nullish(),
|
||||||
|
type_id: z.string().nullish(),
|
||||||
|
external_id: z.string().nullish(),
|
||||||
|
collection_id: z.string().nullish(),
|
||||||
|
categories: z.array(IdAssociation).optional(),
|
||||||
|
tags: z.array(IdAssociation).optional(),
|
||||||
|
sales_channels: z.array(z.object({ id: z.string() })).optional(),
|
||||||
|
shipping_profile_id: z.string().nullish(),
|
||||||
|
weight: numericString().nullish(),
|
||||||
|
length: numericString().nullish(),
|
||||||
|
height: numericString().nullish(),
|
||||||
|
width: numericString().nullish(),
|
||||||
|
hs_code: z.string().nullish(),
|
||||||
|
mid_code: z.string().nullish(),
|
||||||
|
origin_country: z.string().nullish(),
|
||||||
|
material: z.string().nullish(),
|
||||||
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
11
yarn.lock
11
yarn.lock
@@ -23530,6 +23530,7 @@ __metadata:
|
|||||||
"@swc/jest": ^0.2.36
|
"@swc/jest": ^0.2.36
|
||||||
form-data: ^4.0.0
|
form-data: ^4.0.0
|
||||||
jest: ^29.7.0
|
jest: ^29.7.0
|
||||||
|
json-2-csv: ^5.5.9
|
||||||
jsonwebtoken: ^9.0.2
|
jsonwebtoken: ^9.0.2
|
||||||
pg: ^8.11.3
|
pg: ^8.11.3
|
||||||
typescript: ^5.6.2
|
typescript: ^5.6.2
|
||||||
@@ -25062,6 +25063,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"json-2-csv@npm:^5.5.9":
|
||||||
|
version: 5.5.9
|
||||||
|
resolution: "json-2-csv@npm:5.5.9"
|
||||||
|
dependencies:
|
||||||
|
deeks: 3.1.0
|
||||||
|
doc-path: 4.1.1
|
||||||
|
checksum: 109b7636ba3ef9b8c53580f89280568e1ecd106b6328f44636869a62eac1f9f2db7a52de0eee04832fa20d4bd9c592ad3fe1a4cf06af0eee3ee612b85f29ed75
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"json-buffer@npm:3.0.1":
|
"json-buffer@npm:3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "json-buffer@npm:3.0.1"
|
resolution: "json-buffer@npm:3.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user