@bytebrand/export-cars 1.26.46 → 1.26.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/ooyyo.config.d.ts +14 -0
- package/dist/configs/ooyyo.config.js +40 -6
- package/dist/configs/ooyyo.config.js.map +1 -1
- package/dist/exporters/ooyyo.exporter.d.ts +5 -4
- package/dist/exporters/ooyyo.exporter.js +45 -25
- package/dist/exporters/ooyyo.exporter.js.map +1 -1
- package/dist/formats/ooyyo-xml.builder.d.ts +7 -2
- package/dist/formats/ooyyo-xml.builder.js +59 -36
- package/dist/formats/ooyyo-xml.builder.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +30 -2
- package/dist/interfaces/params.interface.d.ts +3 -0
- package/dist/services/mapping.service.d.ts +2 -0
- package/dist/services/mapping.service.js +33 -13
- package/dist/services/mapping.service.js.map +1 -1
- package/dist/services/ooyyo-feed-run.d.ts +81 -0
- package/dist/services/ooyyo-feed-run.js +183 -0
- package/dist/services/ooyyo-feed-run.js.map +1 -0
- package/dist/services/ooyyo-feed-verify.d.ts +13 -0
- package/dist/services/ooyyo-feed-verify.js +104 -0
- package/dist/services/ooyyo-feed-verify.js.map +1 -0
- package/dist/transformers/ooyyo.transformer.d.ts +51 -11
- package/dist/transformers/ooyyo.transformer.js +268 -95
- package/dist/transformers/ooyyo.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/marktde/AUDIT.md +1 -1
- package/docs/marktde/RUNBOOK.md +3 -3
- package/docs/marktde/TRACKING.md +8 -2
- package/docs/meinestadt/PARTNER-QUESTIONS.md +17 -0
- package/docs/meinestadt/TRACKING.md +107 -0
- package/docs/ooyyo/AUDIT.md +81 -0
- package/docs/ooyyo/PARTNER-QUESTIONS.md +32 -0
- package/docs/ooyyo/RUNBOOK.md +67 -0
- package/docs/ooyyo/TRACKING.md +54 -0
- package/package.json +3 -2
- package/src/configs/ooyyo.config.ts +65 -15
- package/src/constants/fb.constants.ts +34 -34
- package/src/exporters/ooyyo.exporter.ts +75 -48
- package/src/formats/ooyyo-xml.builder.ts +87 -48
- package/src/interfaces/export-item.interface.ts +43 -2
- package/src/interfaces/params.interface.ts +5 -0
- package/src/services/mapping.service.ts +54 -13
- package/src/services/ooyyo-feed-run.ts +265 -0
- package/src/services/ooyyo-feed-verify.ts +131 -0
- package/src/transformers/ooyyo.transformer.ts +378 -109
- package/test/fixtures/ooyyo/OoyyoFormat.xml +125 -0
- package/test/fixtures/ooyyo/OoyyoFormat.xsd +116 -0
- package/test/fixtures/ooyyo.fixture.ts +53 -0
- package/test/mapping-csv.test.ts +65 -0
- package/test/ooyyo-feed-run.test.ts +293 -0
- package/test/ooyyo-mapping.test.ts +276 -0
|
@@ -1,48 +1,75 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
|
|
4
|
+
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
5
|
+
import { LoggerService } from '@bytebrand/cronicle-helper';
|
|
6
|
+
|
|
7
|
+
import { CarService, RulesService, LoaderService, ConfigService, ImagesService } from '../services';
|
|
8
|
+
import { ImageUrlValidator } from '../services/image-url-validator.service';
|
|
9
|
+
import { FeedGuard } from '../services/feed-guard';
|
|
10
|
+
import { OoyyoFeedRun, OoyyoSourceCar } from '../services/ooyyo-feed-run';
|
|
11
|
+
import { OoyyoTransformer, toOoyyoImages } from '../transformers/ooyyo.transformer';
|
|
12
|
+
import { resolveOpenAiImageFields } from '../transformers/openai-image-fields';
|
|
13
|
+
import { OoyyoConfig } from '../configs/ooyyo.config';
|
|
14
|
+
import { Exporter } from '../interfaces/exporter.interface';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ExportType.OOYYO (ADV-331). Wires the Cronicle/Nest services into OoyyoFeedRun, which holds the
|
|
18
|
+
* logic. The output directory is no longer emptied: the new XML / XML.gz replace the old ones only
|
|
19
|
+
* after the snapshot, the written file and the counts passed.
|
|
20
|
+
*/
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class OoyyoExporter implements Exporter {
|
|
23
|
+
constructor(
|
|
24
|
+
private carService: CarService,
|
|
25
|
+
private configService: ConfigService,
|
|
26
|
+
private loaderService: LoaderService,
|
|
27
|
+
private rulesService: RulesService,
|
|
28
|
+
private imagesService: ImagesService,
|
|
29
|
+
private moduleRef: ModuleRef,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async export(): Promise<void> {
|
|
33
|
+
const config = this.configService.config as OoyyoConfig;
|
|
34
|
+
const [xmlFile, gzFile] = config.outputFiles;
|
|
35
|
+
const validator = new ImageUrlValidator({ concurrency: config.imageConcurrency });
|
|
36
|
+
const transformer = await this.moduleRef.create(OoyyoTransformer);
|
|
37
|
+
|
|
38
|
+
const run = new OoyyoFeedRun(
|
|
39
|
+
{
|
|
40
|
+
forEachChunk: fn => this.loaderService.process((chunk: FsChunkInterface) => fn(chunk.data as OoyyoSourceCar[])),
|
|
41
|
+
filterCar: source => this.carService.filterCar(source, this.rulesService.rules),
|
|
42
|
+
resolveImages: async ({ car, images }) => toOoyyoImages(
|
|
43
|
+
await resolveOpenAiImageFields(this.imagesService.getCleanImages(car._id, images), validator),
|
|
44
|
+
config.maxImages,
|
|
45
|
+
),
|
|
46
|
+
translate: selector => transformer.translate(selector),
|
|
47
|
+
region: (postcode, country) => transformer.region(postcode, country),
|
|
48
|
+
imageStats: () => validator.stats,
|
|
49
|
+
guard: new FeedGuard(config.feedGuard),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
runId: config.runId,
|
|
53
|
+
dryRun: config.dryRun,
|
|
54
|
+
domain: config.domain,
|
|
55
|
+
sourceDirectory: config.pathesConfig.localDirectory,
|
|
56
|
+
sourceBaseFileName: config.pathesConfig.baseFileName,
|
|
57
|
+
xmlFile,
|
|
58
|
+
gzFile,
|
|
59
|
+
stateDir: config.stateDir,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const report = await run.run();
|
|
65
|
+
if (report.stale) {
|
|
66
|
+
console.log(`ooyyo feed STALE: last good file ${report.lastGoodAgeHours?.toFixed(1)} h old`);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// main.ts would report a rejected run as {"complete":1,"code":0}; fail the job so the chained
|
|
70
|
+
// `6.1 … part 2` step does not run (report + lock already handled by the run).
|
|
71
|
+
LoggerService.exitError(`ooyyo feed not published: ${error?.message ?? error}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,48 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OoyyoXmlItem } from '../interfaces';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
import { createWriteStream, WriteStream } from 'fs';
|
|
2
|
+
import { OoyyoXmlItem } from '../interfaces';
|
|
3
|
+
|
|
4
|
+
// Characters XML 1.0 does not allow at all (a single one makes the whole file unreadable).
|
|
5
|
+
// eslint-disable-next-line no-control-regex
|
|
6
|
+
const INVALID_XML_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
7
|
+
|
|
8
|
+
export const stripInvalidXmlChars = (value: string): string => value.replace(INVALID_XML_CHARS, '');
|
|
9
|
+
|
|
10
|
+
// CDATA cannot contain "]]>": split it across two sections. Everything else goes in verbatim
|
|
11
|
+
// (UTF-8 umlauts, "&" in URLs), exactly as the partner sample wraps every value in CDATA.
|
|
12
|
+
export const cdata = (value: string): string =>
|
|
13
|
+
`<![CDATA[${stripInvalidXmlChars(value).replace(/]]>/g, ']]]]><![CDATA[>')}]]>`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Writes the OOYYO XML (ADV-331): UTF-8, root/namespace exactly as OoyyoFormat.xml, element order as
|
|
17
|
+
* in the sample, every value in CDATA. Empty optional elements are left out — the legacy builder
|
|
18
|
+
* wrote `<engine_size/>` / `<power/>` without the unit attribute the XSD requires, so such a file was
|
|
19
|
+
* not schema-valid. `price` always carries unit="EUR".
|
|
20
|
+
*/
|
|
21
|
+
export class OoyyoXmlBuilder {
|
|
22
|
+
private stream: WriteStream;
|
|
23
|
+
|
|
24
|
+
constructor(fileName: string) {
|
|
25
|
+
this.stream = createWriteStream(fileName, { flags: 'w', encoding: 'utf8' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private write(chunk: string): Promise<void> {
|
|
29
|
+
return new Promise((resolve, reject) => this.stream.write(chunk, error => (error ? reject(error) : resolve())));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async buildStart(): Promise<void> {
|
|
33
|
+
await this.write('<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
34
|
+
+ '<ooyyo xsi:noNamespaceSchemaLocation="OoyyoFormat.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async addItem(car: OoyyoXmlItem): Promise<void> {
|
|
38
|
+
await this.write(OoyyoXmlBuilder.toXml(car));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static toXml(car: OoyyoXmlItem): string {
|
|
42
|
+
const el = (tag: string, value: string, unit?: string, indent = ' '): string =>
|
|
43
|
+
(value ? `${indent}<${tag}${unit ? ` unit="${unit}"` : ''}>${cdata(value)}</${tag}>\n` : '');
|
|
44
|
+
const location = [
|
|
45
|
+
el('city', car.location?.city, undefined, ' '),
|
|
46
|
+
el('postcode', car.location?.postcode, undefined, ' '),
|
|
47
|
+
el('region', car.location?.region, undefined, ' '),
|
|
48
|
+
].join('');
|
|
49
|
+
const pictures = (car.pictures ?? []).map(url => el('picture', url, undefined, ' ')).join('');
|
|
50
|
+
|
|
51
|
+
return ' <ad>\n'
|
|
52
|
+
+ el('url', car.url)
|
|
53
|
+
+ el('title', car.title)
|
|
54
|
+
+ el('content', car.content)
|
|
55
|
+
+ el('price', car.price, car.priceUnit)
|
|
56
|
+
+ el('make', car.make)
|
|
57
|
+
+ el('model', car.model)
|
|
58
|
+
+ el('engine_size', car.engine_size, 'cc')
|
|
59
|
+
+ el('trim', car.trim)
|
|
60
|
+
+ el('body_type', car.body_type)
|
|
61
|
+
+ el('color', car.color)
|
|
62
|
+
+ el('year', car.year)
|
|
63
|
+
+ el('mileage', car.mileage, 'km')
|
|
64
|
+
+ el('fuel_type', car.fuel_type)
|
|
65
|
+
+ el('transmission', car.transmission)
|
|
66
|
+
+ el('euronorm', car.euronorm)
|
|
67
|
+
+ el('power', car.power, 'kw')
|
|
68
|
+
+ el('doors', car.doors)
|
|
69
|
+
+ el('seats', car.seats)
|
|
70
|
+
+ el('gears', car.gears)
|
|
71
|
+
+ (location ? ` <location>\n${location} </location>\n` : '')
|
|
72
|
+
+ (pictures ? ` <pictures>\n${pictures} </pictures>\n` : '')
|
|
73
|
+
+ el('vin', car.vin)
|
|
74
|
+
+ el('is_new', car.is_new)
|
|
75
|
+
+ el('date', car.date)
|
|
76
|
+
+ el('time', car.time)
|
|
77
|
+
+ ' </ad>\n';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async buildEnd(): Promise<void> {
|
|
81
|
+
await this.write('</ooyyo>\n');
|
|
82
|
+
await new Promise<void>((resolve, reject) => {
|
|
83
|
+
this.stream.on('error', reject);
|
|
84
|
+
this.stream.end(() => resolve());
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -62,9 +62,50 @@ export interface Auto24XmlItem {
|
|
|
62
62
|
dealer_phone: string;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// One OOYYO <ad> (ADV-331). '' = element left out (every element is optional in OoyyoFormat.xsd;
|
|
66
|
+
// the importer's mandatory fields are enforced by validateOoyyoItem).
|
|
67
|
+
export interface OoyyoXmlItem {
|
|
68
|
+
url: string;
|
|
69
|
+
title: string;
|
|
70
|
+
content: string;
|
|
71
|
+
// whole euros
|
|
72
|
+
price: string;
|
|
73
|
+
// always 'EUR' (partner, 19.10.2026)
|
|
74
|
+
priceUnit: 'EUR';
|
|
75
|
+
make: string;
|
|
76
|
+
model: string;
|
|
77
|
+
// ccm, unit="cc"
|
|
78
|
+
engine_size: string;
|
|
79
|
+
trim: string;
|
|
80
|
+
body_type: string;
|
|
81
|
+
color: string;
|
|
82
|
+
year: string;
|
|
83
|
+
// unit="km"
|
|
67
84
|
mileage: string;
|
|
85
|
+
fuel_type: string;
|
|
86
|
+
transmission: string;
|
|
87
|
+
euronorm: string;
|
|
88
|
+
// unit="kw"
|
|
89
|
+
power: string;
|
|
90
|
+
doors: string;
|
|
91
|
+
seats: string;
|
|
92
|
+
gears: string;
|
|
93
|
+
location: {
|
|
94
|
+
city: string;
|
|
95
|
+
// string, leading zero kept
|
|
96
|
+
postcode: string;
|
|
97
|
+
// Bundesland
|
|
98
|
+
region: string;
|
|
99
|
+
};
|
|
100
|
+
// validated public HTTPS photo URLs, hero first
|
|
101
|
+
pictures: string[];
|
|
102
|
+
vin: string;
|
|
103
|
+
// '1' new, '0' used
|
|
104
|
+
is_new: string;
|
|
105
|
+
// DD/MM/YYYY, Europe/Berlin
|
|
106
|
+
date: string;
|
|
107
|
+
// HH:MM, Europe/Berlin
|
|
108
|
+
time: string;
|
|
68
109
|
}
|
|
69
110
|
|
|
70
111
|
// Keys and order as the meinestadt exporter emits them (see docs/meinestadt/AUDIT.md §3).
|
|
@@ -53,6 +53,11 @@ export interface Params {
|
|
|
53
53
|
marktdeDryRun?: string;
|
|
54
54
|
marktdeImageConcurrency?: string;
|
|
55
55
|
marktdeMaxImages?: string;
|
|
56
|
+
// OOYYO feed only (ADV-331), Cronicle text params; see configs/ooyyo.config.ts.
|
|
57
|
+
// Live output needs ooyyoDryRun=false explicitly.
|
|
58
|
+
ooyyoDryRun?: string;
|
|
59
|
+
ooyyoImageConcurrency?: string;
|
|
60
|
+
ooyyoMaxImages?: string;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
// Parsed from feedItemsBq* by GoogleConfig.
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import axios from 'axios';
|
|
3
|
+
import { CronicleUtils } from '@bytebrand/cronicle-helper';
|
|
3
4
|
import { MappingRules } from '../interfaces/mapping-rules.interface';
|
|
4
5
|
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from 'google-spreadsheet';
|
|
5
6
|
import { ConfigService } from './config.service';
|
|
6
7
|
import { DealersMapping, MetaPushPriorityMapping } from '../interfaces/dealers-mapping.interface';
|
|
7
8
|
import { parseCsvToObjects } from '../utils';
|
|
8
9
|
|
|
10
|
+
// Public CSV of one tab of a Google sheet, addressed by the tab title — no API key. The sheet must stay
|
|
11
|
+
// readable by everyone with the link. (Checked 13.11.2026: gviz CSV = the raw export of the tabs
|
|
12
|
+
// mobile_yescar and mobile, cell for cell; gviz only adds trailing empty columns.)
|
|
13
|
+
export const mappingSheetCsvUrl = (spreadsheetId: string, title: string): string =>
|
|
14
|
+
`https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?tqx=out:csv&sheet=${encodeURIComponent(title)}`;
|
|
15
|
+
|
|
16
|
+
// Columns every mapping tab read by getRules() has; missing ones mean the response is not the tab
|
|
17
|
+
// (e.g. Google's sign-in page once the sheet is no longer shared).
|
|
18
|
+
const REQUIRED_MAPPING_COLUMNS = ['importField', 'importFieldType', 'autodeField', 'autodeFieldType'];
|
|
19
|
+
|
|
9
20
|
@Injectable()
|
|
10
21
|
export class MappingService {
|
|
11
22
|
constructor(private configService: ConfigService) {}
|
|
@@ -13,24 +24,54 @@ export class MappingService {
|
|
|
13
24
|
private dealers: any;
|
|
14
25
|
private metaPushPriorities: MetaPushPriorityMapping[];
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Mapping rules of the export type's tab (mappingSpreadsheetTitle) of the mapping sheet. Read from the
|
|
29
|
+
* sheet's public CSV instead of the Google Sheets API: the API needed the event's `apikey`, and an
|
|
30
|
+
* expired key (DEV 9c.0, found 12.11.2026) silently broke every feed on the sheet. Fails the run
|
|
31
|
+
* when the tab cannot be read or yields no rule — never an export without a mapping.
|
|
32
|
+
*/
|
|
16
33
|
async getRules(): Promise<MappingRules[]> {
|
|
17
34
|
if (!this.rules) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
await
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
const { mappingSpreadsheetId, mappingSpreadsheetTitle } = this.configService.config;
|
|
36
|
+
const url = mappingSheetCsvUrl(mappingSpreadsheetId, mappingSpreadsheetTitle);
|
|
37
|
+
const { data } = await CronicleUtils.retry(
|
|
38
|
+
() => axios.get<string>(url, { responseType: 'text', timeout: 30_000 }),
|
|
39
|
+
3,
|
|
40
|
+
5_000,
|
|
41
|
+
true,
|
|
42
|
+
);
|
|
43
|
+
this.rules = this.rulesFromCsv(data, mappingSpreadsheetTitle);
|
|
44
|
+
console.log(`mapping: ${this.rules.length} rules from the public CSV of tab ${mappingSpreadsheetTitle}`);
|
|
45
|
+
}
|
|
46
|
+
return this.rules;
|
|
47
|
+
}
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
// CSV text of a mapping tab -> rules, with the same post-processing the Sheets API rows had.
|
|
50
|
+
rulesFromCsv(csv: string, title: string): MappingRules[] {
|
|
51
|
+
const rows = parseCsvToObjects<Record<string, string>>(typeof csv === 'string' ? csv : '');
|
|
52
|
+
const columns = rows.length ? Object.keys(rows[0]) : [];
|
|
53
|
+
const missing = REQUIRED_MAPPING_COLUMNS.filter(column => !columns.includes(column));
|
|
54
|
+
if (missing.length) {
|
|
55
|
+
throw new Error(`mapping sheet tab "${title}" not readable as CSV (missing columns ${missing.join(', ')}) — is the sheet still shared with everyone who has the link?`);
|
|
56
|
+
}
|
|
57
|
+
// unnamed columns (notes next to the table) are not part of a rule
|
|
58
|
+
const mappingRules = rows.map((row) => {
|
|
59
|
+
delete row[''];
|
|
60
|
+
return row;
|
|
61
|
+
}) as unknown as MappingRules[];
|
|
62
|
+
|
|
63
|
+
mappingRules.forEach((rule, _i, selfRules) => {
|
|
64
|
+
this.removeEmptyKeys(rule);
|
|
65
|
+
if (rule.importFieldType && rule.importFieldType.includes('selector')) {
|
|
66
|
+
this.groupSelectors(rule, selfRules);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
30
69
|
|
|
31
|
-
|
|
70
|
+
const rules = this.filterResultedRows(mappingRules);
|
|
71
|
+
if (!rules.length) {
|
|
72
|
+
throw new Error(`mapping sheet tab "${title}" has no rule`);
|
|
32
73
|
}
|
|
33
|
-
return
|
|
74
|
+
return rules;
|
|
34
75
|
}
|
|
35
76
|
|
|
36
77
|
async getDealersMapping(): Promise<DealersMapping[]> {
|