|
|
@@ -41,33 +41,53 @@ export interface ParsedProductWithVariants {
|
|
|
variants: ParsedProductVariant[];
|
|
|
}
|
|
|
|
|
|
+export interface ParseResult<T> {
|
|
|
+ results: T[];
|
|
|
+ errors: string[];
|
|
|
+}
|
|
|
+
|
|
|
+const requiredColumns: Array<keyof RawProductRecord> = [
|
|
|
+ 'name',
|
|
|
+ 'slug',
|
|
|
+ 'description',
|
|
|
+ 'assets',
|
|
|
+ 'optionGroups',
|
|
|
+ 'optionValues',
|
|
|
+ 'sku',
|
|
|
+ 'price',
|
|
|
+ 'taxCategory',
|
|
|
+ 'variantAssets',
|
|
|
+];
|
|
|
+
|
|
|
/**
|
|
|
* Validates and parses CSV files into a data structure which can then be used to created new entities.
|
|
|
*/
|
|
|
@Injectable()
|
|
|
export class ImportParser {
|
|
|
- async parseProducts(input: string | Stream): Promise<ParsedProductWithVariants[]> {
|
|
|
+ async parseProducts(input: string | Stream): Promise<ParseResult<ParsedProductWithVariants>> {
|
|
|
const options: parse.Options = {
|
|
|
- columns: true,
|
|
|
trim: true,
|
|
|
+ relax_column_count: true,
|
|
|
};
|
|
|
- return new Promise<ParsedProductWithVariants[]>((resolve, reject) => {
|
|
|
+ return new Promise<ParseResult<ParsedProductWithVariants>>((resolve, reject) => {
|
|
|
+ let errors: string[] = [];
|
|
|
+
|
|
|
if (typeof input === 'string') {
|
|
|
- parse(input, options, (err, records: RawProductRecord[]) => {
|
|
|
+ parse(input, options, (err: any, records: string[][]) => {
|
|
|
if (err) {
|
|
|
- reject(err);
|
|
|
+ errors = errors.concat(err);
|
|
|
}
|
|
|
-
|
|
|
- try {
|
|
|
- const output = this.processRawRecords(records);
|
|
|
- resolve(output);
|
|
|
- } catch (err) {
|
|
|
- reject(err);
|
|
|
+ if (records) {
|
|
|
+ const parseResult = this.processRawRecords(records);
|
|
|
+ errors = errors.concat(parseResult.errors);
|
|
|
+ resolve({ results: parseResult.results, errors });
|
|
|
+ } else {
|
|
|
+ resolve({ results: [], errors });
|
|
|
}
|
|
|
});
|
|
|
} else {
|
|
|
const parser = parse(options);
|
|
|
- const records: RawProductRecord[] = [];
|
|
|
+ const records: string[][] = [];
|
|
|
// input.on('open', () => input.pipe(parser));
|
|
|
input.pipe(parser);
|
|
|
parser.on('readable', () => {
|
|
|
@@ -79,26 +99,36 @@ export class ImportParser {
|
|
|
});
|
|
|
parser.on('error', reject);
|
|
|
parser.on('end', () => {
|
|
|
- try {
|
|
|
- const output = this.processRawRecords(records);
|
|
|
- resolve(output);
|
|
|
- } catch (err) {
|
|
|
- reject(err);
|
|
|
- }
|
|
|
+ const parseResult = this.processRawRecords(records);
|
|
|
+ errors = errors.concat(parseResult.errors);
|
|
|
+ resolve({ results: parseResult.results, errors });
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private processRawRecords(records: RawProductRecord[]): ParsedProductWithVariants[] {
|
|
|
- const output: ParsedProductWithVariants[] = [];
|
|
|
+ private processRawRecords(records: string[][]): ParseResult<ParsedProductWithVariants> {
|
|
|
+ const results: ParsedProductWithVariants[] = [];
|
|
|
+ const errors: string[] = [];
|
|
|
let currentRow: ParsedProductWithVariants | undefined;
|
|
|
- validateColumns(records[0]);
|
|
|
- for (const r of records) {
|
|
|
+ const headerRow = records.shift() as string[];
|
|
|
+ const columnError = validateRequiredColumns(headerRow);
|
|
|
+ if (columnError) {
|
|
|
+ return { results: [], errors: [columnError] };
|
|
|
+ }
|
|
|
+ let line = 1;
|
|
|
+ for (const record of records) {
|
|
|
+ line++;
|
|
|
+ const columnCountError = validateColumnCount(headerRow, record);
|
|
|
+ if (columnCountError) {
|
|
|
+ errors.push(columnCountError + ` on line ${line}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const r = mapRowToObject(headerRow, record);
|
|
|
if (r.name) {
|
|
|
if (currentRow) {
|
|
|
populateOptionGroupValues(currentRow);
|
|
|
- output.push(currentRow);
|
|
|
+ results.push(currentRow);
|
|
|
}
|
|
|
currentRow = {
|
|
|
product: parseProductFromRecord(r),
|
|
|
@@ -109,13 +139,16 @@ export class ImportParser {
|
|
|
currentRow.variants.push(parseVariantFromRecord(r));
|
|
|
}
|
|
|
}
|
|
|
- validateOptionValueCount(r, currentRow);
|
|
|
+ const optionError = validateOptionValueCount(r, currentRow);
|
|
|
+ if (optionError) {
|
|
|
+ errors.push(optionError + ` on line ${line}`);
|
|
|
+ }
|
|
|
}
|
|
|
if (currentRow) {
|
|
|
populateOptionGroupValues(currentRow);
|
|
|
- output.push(currentRow);
|
|
|
+ results.push(currentRow);
|
|
|
}
|
|
|
- return output;
|
|
|
+ return { results, errors };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -127,20 +160,8 @@ function populateOptionGroupValues(currentRow: ParsedProductWithVariants) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function validateColumns(r: RawProductRecord) {
|
|
|
- const requiredColumns: Array<keyof RawProductRecord> = [
|
|
|
- 'name',
|
|
|
- 'slug',
|
|
|
- 'description',
|
|
|
- 'assets',
|
|
|
- 'optionGroups',
|
|
|
- 'optionValues',
|
|
|
- 'sku',
|
|
|
- 'price',
|
|
|
- 'taxCategory',
|
|
|
- 'variantAssets',
|
|
|
- ];
|
|
|
- const rowKeys = Object.keys(r);
|
|
|
+function validateRequiredColumns(r: string[]): string | undefined {
|
|
|
+ const rowKeys = r;
|
|
|
const missing: string[] = [];
|
|
|
for (const col of requiredColumns) {
|
|
|
if (!rowKeys.includes(col)) {
|
|
|
@@ -148,21 +169,32 @@ function validateColumns(r: RawProductRecord) {
|
|
|
}
|
|
|
}
|
|
|
if (missing.length) {
|
|
|
- throw new Error(
|
|
|
- `The import file is missing the following columns: ${missing.map(m => `"${m}"`).join(', ')}`,
|
|
|
- );
|
|
|
+ return `The import file is missing the following columns: ${missing.map(m => `"${m}"`).join(', ')}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function validateOptionValueCount(r: RawProductRecord, currentRow?: ParsedProductWithVariants) {
|
|
|
+function validateColumnCount(columns: string[], row: string[]): string | undefined {
|
|
|
+ if (columns.length !== row.length) {
|
|
|
+ return `Invalid Record Length: header length is ${columns.length}, got ${row.length}`;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function mapRowToObject(columns: string[], row: string[]): { [key: string]: string } {
|
|
|
+ return row.reduce((obj, val, i) => {
|
|
|
+ return { ...obj, [columns[i]]: val };
|
|
|
+ }, {});
|
|
|
+}
|
|
|
+
|
|
|
+function validateOptionValueCount(
|
|
|
+ r: RawProductRecord,
|
|
|
+ currentRow?: ParsedProductWithVariants,
|
|
|
+): string | undefined {
|
|
|
if (!currentRow) {
|
|
|
return;
|
|
|
}
|
|
|
const optionValues = parseStringArray(r.optionValues);
|
|
|
if (currentRow.product.optionGroups.length !== optionValues.length) {
|
|
|
- throw new Error(
|
|
|
- `The number of optionValues must match the number of optionGroups for the product "${r.name}"`,
|
|
|
- );
|
|
|
+ return `The number of optionValues must match the number of optionGroups`;
|
|
|
}
|
|
|
}
|
|
|
|