Browse Source

feat(server): Implement importing of custom fields

Michael Bromley 7 years ago
parent
commit
c564beaa73

+ 1 - 1
server/e2e/import.e2e-spec.ts

@@ -30,7 +30,7 @@ describe('Import resolver', () => {
         // Session repository called from the AuthService.validateSession() method.
         // Session repository called from the AuthService.validateSession() method.
         // After several hours of fruitless hunting, I did what any desperate JavaScript
         // After several hours of fruitless hunting, I did what any desperate JavaScript
         // developer would do, and threw in a setTimeout. Which of course "works"...
         // developer would do, and threw in a setTimeout. Which of course "works"...
-        const timeout = process.env.CI ? 2000 : 500;
+        const timeout = process.env.CI ? 2000 : 1000;
         await new Promise(resolve => {
         await new Promise(resolve => {
             setTimeout(resolve, timeout);
             setTimeout(resolve, timeout);
         });
         });

+ 102 - 0
server/src/data-import/providers/import-parser/__snapshots__/import-parser.spec.ts.snap

@@ -1,10 +1,79 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
+exports[`ImportParser parseProducts custom fields 1`] = `
+Array [
+  Object {
+    "product": Object {
+      "assetPaths": Array [],
+      "customFields": Object {
+        "customPage": "grid-view",
+        "keywords": "paper, stretch",
+      },
+      "description": "A great device for stretching paper.",
+      "name": "Perfect Paper Stretcher",
+      "optionGroups": Array [
+        Object {
+          "name": "size",
+          "values": Array [
+            "Half Imperial",
+            "Quarter Imperial",
+            "Full Imperial",
+          ],
+        },
+      ],
+      "slug": "perfect-paper-stretcher",
+    },
+    "variants": Array [
+      Object {
+        "assetPaths": Array [],
+        "customFields": Object {
+          "volumetric": "243",
+        },
+        "facets": Array [],
+        "optionValues": Array [
+          "Half Imperial",
+        ],
+        "price": 45.3,
+        "sku": "PPS12",
+        "taxCategory": "standard",
+      },
+      Object {
+        "assetPaths": Array [],
+        "customFields": Object {
+          "volumetric": "344",
+        },
+        "facets": Array [],
+        "optionValues": Array [
+          "Quarter Imperial",
+        ],
+        "price": 32.5,
+        "sku": "PPS14",
+        "taxCategory": "standard",
+      },
+      Object {
+        "assetPaths": Array [],
+        "customFields": Object {
+          "volumetric": "656",
+        },
+        "facets": Array [],
+        "optionValues": Array [
+          "Full Imperial",
+        ],
+        "price": 59.5,
+        "sku": "PPSF",
+        "taxCategory": "standard",
+      },
+    ],
+  },
+]
+`;
+
 exports[`ImportParser parseProducts multiple products with multiple variants 1`] = `
 exports[`ImportParser parseProducts multiple products with multiple variants 1`] = `
 Array [
 Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "A great device for stretching paper.",
       "description": "A great device for stretching paper.",
       "name": "Perfect Paper Stretcher",
       "name": "Perfect Paper Stretcher",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -22,6 +91,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Half Imperial",
           "Half Imperial",
@@ -32,6 +102,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Quarter Imperial",
           "Quarter Imperial",
@@ -42,6 +113,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Full Imperial",
           "Full Imperial",
@@ -55,6 +127,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "Mabef description",
       "description": "Mabef description",
       "name": "Mabef M/02 Studio Easel",
       "name": "Mabef M/02 Studio Easel",
       "optionGroups": Array [],
       "optionGroups": Array [],
@@ -63,6 +136,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [],
         "optionValues": Array [],
         "price": 910.7,
         "price": 910.7,
@@ -74,6 +148,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "Really mega pencils",
       "description": "Really mega pencils",
       "name": "Giotto Mega Pencils",
       "name": "Giotto Mega Pencils",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -90,6 +165,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Box of 8",
           "Box of 8",
@@ -100,6 +176,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Box of 12",
           "Box of 12",
@@ -113,6 +190,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "Keeps the paint off the clothes",
       "description": "Keeps the paint off the clothes",
       "name": "Artists Smock",
       "name": "Artists Smock",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -136,6 +214,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "small",
           "small",
@@ -147,6 +226,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "large",
           "large",
@@ -158,6 +238,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "small",
           "small",
@@ -169,6 +250,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "large",
           "large",
@@ -188,6 +270,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "A great device for stretching paper.",
       "description": "A great device for stretching paper.",
       "name": "Perfect Paper Stretcher",
       "name": "Perfect Paper Stretcher",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -205,6 +288,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Half Imperial",
           "Half Imperial",
@@ -215,6 +299,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Quarter Imperial",
           "Quarter Imperial",
@@ -225,6 +310,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Full Imperial",
           "Full Imperial",
@@ -246,6 +332,7 @@ Array [
         "pps1.jpg",
         "pps1.jpg",
         "pps2.jpg",
         "pps2.jpg",
       ],
       ],
+      "customFields": Object {},
       "description": "A great device for stretching paper.",
       "description": "A great device for stretching paper.",
       "name": "Perfect Paper Stretcher",
       "name": "Perfect Paper Stretcher",
       "optionGroups": Array [],
       "optionGroups": Array [],
@@ -254,6 +341,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [
         "facets": Array [
           Object {
           Object {
             "facet": "brand",
             "facet": "brand",
@@ -279,6 +367,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "A great device for stretching paper.",
       "description": "A great device for stretching paper.",
       "name": "Perfect Paper Stretcher",
       "name": "Perfect Paper Stretcher",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -296,6 +385,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Half Imperial",
           "Half Imperial",
@@ -306,6 +396,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Quarter Imperial",
           "Quarter Imperial",
@@ -316,6 +407,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Full Imperial",
           "Full Imperial",
@@ -329,6 +421,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "Mabef description",
       "description": "Mabef description",
       "name": "Mabef M/02 Studio Easel",
       "name": "Mabef M/02 Studio Easel",
       "optionGroups": Array [],
       "optionGroups": Array [],
@@ -337,6 +430,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [],
         "optionValues": Array [],
         "price": 910.7,
         "price": 910.7,
@@ -348,6 +442,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "Really mega pencils",
       "description": "Really mega pencils",
       "name": "Giotto Mega Pencils",
       "name": "Giotto Mega Pencils",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -364,6 +459,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Box of 8",
           "Box of 8",
@@ -374,6 +470,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "Box of 12",
           "Box of 12",
@@ -387,6 +484,7 @@ Array [
   Object {
   Object {
     "product": Object {
     "product": Object {
       "assetPaths": Array [],
       "assetPaths": Array [],
+      "customFields": Object {},
       "description": "Keeps the paint off the clothes",
       "description": "Keeps the paint off the clothes",
       "name": "Artists Smock",
       "name": "Artists Smock",
       "optionGroups": Array [
       "optionGroups": Array [
@@ -410,6 +508,7 @@ Array [
     "variants": Array [
     "variants": Array [
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "small",
           "small",
@@ -421,6 +520,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "large",
           "large",
@@ -432,6 +532,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "small",
           "small",
@@ -443,6 +544,7 @@ Array [
       },
       },
       Object {
       Object {
         "assetPaths": Array [],
         "assetPaths": Array [],
+        "customFields": Object {},
         "facets": Array [],
         "facets": Array [],
         "optionValues": Array [
         "optionValues": Array [
           "large",
           "large",

+ 9 - 0
server/src/data-import/providers/import-parser/import-parser.spec.ts

@@ -32,6 +32,15 @@ describe('ImportParser', () => {
             expect(result.results).toMatchSnapshot();
             expect(result.results).toMatchSnapshot();
         });
         });
 
 
+        it('custom fields', async () => {
+            const importParser = new ImportParser();
+
+            const input = await loadTestFixture('custom-fields.csv');
+            const result = await importParser.parseProducts(input);
+
+            expect(result.results).toMatchSnapshot();
+        });
+
         it('works with streamed input', async () => {
         it('works with streamed input', async () => {
             const importParser = new ImportParser();
             const importParser = new ImportParser();
 
 

+ 28 - 4
server/src/data-import/providers/import-parser/import-parser.ts

@@ -4,7 +4,7 @@ import { Stream } from 'stream';
 
 
 import { normalizeString } from '../../../../../shared/normalize-string';
 import { normalizeString } from '../../../../../shared/normalize-string';
 
 
-export interface RawProductRecord {
+export type BaseProductRecord = {
     name?: string;
     name?: string;
     slug?: string;
     slug?: string;
     description?: string;
     description?: string;
@@ -16,7 +16,9 @@ export interface RawProductRecord {
     taxCategory?: string;
     taxCategory?: string;
     variantAssets?: string;
     variantAssets?: string;
     facets?: string;
     facets?: string;
-}
+};
+
+export type RawProductRecord = BaseProductRecord & { [customFieldName: string]: string };
 
 
 export interface ParsedProductVariant {
 export interface ParsedProductVariant {
     optionValues: string[];
     optionValues: string[];
@@ -28,6 +30,9 @@ export interface ParsedProductVariant {
         facet: string;
         facet: string;
         value: string;
         value: string;
     }>;
     }>;
+    customFields: {
+        [name: string]: string;
+    };
 }
 }
 
 
 export interface ParsedProduct {
 export interface ParsedProduct {
@@ -39,6 +44,9 @@ export interface ParsedProduct {
         name: string;
         name: string;
         values: string[];
         values: string[];
     }>;
     }>;
+    customFields: {
+        [name: string]: string;
+    };
 }
 }
 
 
 export interface ParsedProductWithVariants {
 export interface ParsedProductWithVariants {
@@ -52,7 +60,7 @@ export interface ParseResult<T> {
     processed: number;
     processed: number;
 }
 }
 
 
-const requiredColumns: Array<keyof RawProductRecord> = [
+const requiredColumns: Array<keyof BaseProductRecord> = [
     'name',
     'name',
     'slug',
     'slug',
     'description',
     'description',
@@ -195,7 +203,7 @@ function mapRowToObject(columns: string[], row: string[]): { [key: string]: stri
 }
 }
 
 
 function validateOptionValueCount(
 function validateOptionValueCount(
-    r: RawProductRecord,
+    r: BaseProductRecord,
     currentRow?: ParsedProductWithVariants,
     currentRow?: ParsedProductWithVariants,
 ): string | undefined {
 ): string | undefined {
     if (!currentRow) {
     if (!currentRow) {
@@ -219,6 +227,7 @@ function parseProductFromRecord(r: RawProductRecord): ParsedProduct {
             name: ogName,
             name: ogName,
             values: [],
             values: [],
         })),
         })),
+        customFields: parseCustomFields('product', r),
     };
     };
 }
 }
 
 
@@ -233,9 +242,24 @@ function parseVariantFromRecord(r: RawProductRecord): ParsedProductVariant {
             const [facet, value] = pair.split(':');
             const [facet, value] = pair.split(':');
             return { facet, value };
             return { facet, value };
         }),
         }),
+        customFields: parseCustomFields('variant', r),
     };
     };
 }
 }
 
 
+function parseCustomFields(prefix: 'product' | 'variant', r: RawProductRecord): { [name: string]: string } {
+    return Object.entries(r)
+        .filter(([key, value]) => {
+            return key.indexOf(`${prefix}:`) === 0;
+        })
+        .reduce((output, [key, value]) => {
+            const fieldName = key.replace(`${prefix}:`, '');
+            return {
+                ...output,
+                [fieldName]: value,
+            };
+        }, {});
+}
+
 function parseString(input?: string): string {
 function parseString(input?: string): string {
     return (input || '').trim();
     return (input || '').trim();
 }
 }

+ 4 - 0
server/src/data-import/providers/import-parser/test-fixtures/custom-fields.csv

@@ -0,0 +1,4 @@
+name                    , slug , description                          , assets , optionGroups , optionValues     , sku   , price , taxCategory , variantAssets , facets , product:keywords , product:customPage , variant:volumetric
+Perfect Paper Stretcher ,      , A great device for stretching paper. ,        , size         , Half Imperial    , PPS12 , 45.3  , standard    ,               ,        , "paper, stretch" , grid-view          , 243
+                        ,      ,                                      ,        ,              , Quarter Imperial , PPS14 , 32.5  , standard    ,               ,        ,                  ,                    , 344
+                        ,      ,                                      ,        ,              , Full Imperial    , PPSF  , 59.5  , standard    ,               ,        ,                  ,                    , 656

+ 2 - 0
server/src/data-import/providers/importer/importer.ts

@@ -150,6 +150,7 @@ export class Importer {
                         slug: product.slug,
                         slug: product.slug,
                     },
                     },
                 ],
                 ],
+                customFields: product.customFields,
             });
             });
 
 
             const optionsMap: { [optionName: string]: string } = {};
             const optionsMap: { [optionName: string]: string } = {};
@@ -204,6 +205,7 @@ export class Importer {
                         },
                         },
                     ],
                     ],
                     price: Math.round(variant.price * 100),
                     price: Math.round(variant.price * 100),
+                    customFields: variant.customFields,
                 });
                 });
             }
             }
             imported++;
             imported++;