app-manager-edge-worker 1.0.4 → 1.0.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-manager-edge-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -287,6 +287,142 @@ export async function handleInspectElement(payload) {
|
|
|
287
287
|
|
|
288
288
|
const text = el.innerText ? el.innerText.trim() : el.textContent?.trim() || "";
|
|
289
289
|
|
|
290
|
+
// ─── Table Detection (US-01, US-02, US-03) ───
|
|
291
|
+
let tableDetection = null;
|
|
292
|
+
const tableEl = el.closest("table");
|
|
293
|
+
if (tableEl) {
|
|
294
|
+
const rows = Array.from(tableEl.querySelectorAll("tr"));
|
|
295
|
+
if (rows.length > 0) {
|
|
296
|
+
let tableSelector = "table";
|
|
297
|
+
if (tableEl.id) {
|
|
298
|
+
tableSelector = `table#${tableEl.id}`;
|
|
299
|
+
} else if (tableEl.className && typeof tableEl.className === "string") {
|
|
300
|
+
const cls = tableEl.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c));
|
|
301
|
+
if (cls.length > 0) tableSelector = `table.${cls.slice(0, 2).join(".")}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const theadThs = Array.from(tableEl.querySelectorAll("thead th, thead td"));
|
|
305
|
+
const firstRowCells = Array.from(rows[0].querySelectorAll("th, td"));
|
|
306
|
+
const headerCells = theadThs.length > 0 ? theadThs : firstRowCells;
|
|
307
|
+
|
|
308
|
+
const columns = headerCells.map((c, colIdx) => {
|
|
309
|
+
const rawHeader = (c.innerText?.trim() || c.textContent?.trim() || "").replace(/\s+/g, " ");
|
|
310
|
+
const normalizedKey = rawHeader.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || `column_${colIdx + 1}`;
|
|
311
|
+
return { key: normalizedKey, label: rawHeader || `Column ${colIdx + 1}`, columnIndex: colIdx, type: "string", sampleValues: [] };
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const sampleRows = [];
|
|
315
|
+
for (const dr of rows.slice(1, 6)) {
|
|
316
|
+
const cells = Array.from(dr.querySelectorAll("td, th")).map(c => c.innerText?.trim() || c.textContent?.trim() || "");
|
|
317
|
+
const rec = {};
|
|
318
|
+
columns.forEach((col, idx) => {
|
|
319
|
+
rec[col.key] = cells[idx] !== undefined ? cells[idx] : "";
|
|
320
|
+
});
|
|
321
|
+
sampleRows.push(rec);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
tableDetection = {
|
|
325
|
+
isTable: true,
|
|
326
|
+
tableSelector,
|
|
327
|
+
hasHeaders: true,
|
|
328
|
+
headerRowIndex: 0,
|
|
329
|
+
totalRows: rows.length,
|
|
330
|
+
columns,
|
|
331
|
+
sampleRows,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ─── Repeating Structure Detection (US-04, US-05, US-23) ───
|
|
337
|
+
let repeatingDetection = null;
|
|
338
|
+
if (!tableEl) {
|
|
339
|
+
let currR = el;
|
|
340
|
+
let depth = 0;
|
|
341
|
+
while (currR && currR !== document.body && depth < 5) {
|
|
342
|
+
const parent = currR.parentElement;
|
|
343
|
+
if (parent && parent !== document.documentElement) {
|
|
344
|
+
const tag = currR.tagName.toLowerCase();
|
|
345
|
+
const classes = (currR.className && typeof currR.className === "string")
|
|
346
|
+
? currR.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
347
|
+
: [];
|
|
348
|
+
let selCandidate = tag;
|
|
349
|
+
if (classes.length > 0) selCandidate = `${tag}.${classes.slice(0, 2).join(".")}`;
|
|
350
|
+
|
|
351
|
+
const siblings = Array.from(parent.children).filter(c => {
|
|
352
|
+
if (c.tagName.toLowerCase() !== tag) return false;
|
|
353
|
+
if (classes.length > 0 && c.classList) {
|
|
354
|
+
return classes.some(cls => c.classList.contains(cls));
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (siblings.length >= 2) {
|
|
360
|
+
let parentSel = parent.tagName.toLowerCase();
|
|
361
|
+
if (parent.id) parentSel = `#${parent.id}`;
|
|
362
|
+
else if (parent.className && typeof parent.className === "string") {
|
|
363
|
+
const pCls = parent.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c));
|
|
364
|
+
if (pCls.length > 0) parentSel = `${parentSel}.${pCls.slice(0, 2).join(".")}`;
|
|
365
|
+
}
|
|
366
|
+
const containerSelector = `${parentSel} > ${selCandidate}`;
|
|
367
|
+
|
|
368
|
+
const repCard = siblings[0];
|
|
369
|
+
const suggestedFields = [];
|
|
370
|
+
|
|
371
|
+
const heading = repCard.querySelector("h1, h2, h3, h4, h5, .title, [class*='title'], .name, [class*='name']");
|
|
372
|
+
if (heading) {
|
|
373
|
+
const tagH = heading.tagName.toLowerCase();
|
|
374
|
+
const clsH = heading.className && typeof heading.className === "string" ? heading.className.split(/\s+/)[0] : "";
|
|
375
|
+
suggestedFields.push({ key: "title", label: "Title", relativeSelector: clsH ? `.${clsH}` : tagH, attribute: "text", type: "string" });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const priceEl = repCard.querySelector(".price, [class*='price'], [data-price]");
|
|
379
|
+
if (priceEl) {
|
|
380
|
+
const clsP = priceEl.className && typeof priceEl.className === "string" ? priceEl.className.split(/\s+/)[0] : "";
|
|
381
|
+
suggestedFields.push({ key: "price", label: "Price", relativeSelector: clsP ? `.${clsP}` : "[class*='price']", attribute: "text", type: "number" });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const imgEl = repCard.querySelector("img");
|
|
385
|
+
if (imgEl && imgEl.getAttribute("src")) {
|
|
386
|
+
suggestedFields.push({ key: "image", label: "Image URL", relativeSelector: "img", attribute: "src", type: "string" });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const linkEl = repCard.querySelector("a[href]") || (repCard.tagName.toLowerCase() === "a" ? repCard : null);
|
|
390
|
+
if (linkEl && linkEl.getAttribute("href")) {
|
|
391
|
+
suggestedFields.push({ key: "url", label: "Link", relativeSelector: repCard.tagName.toLowerCase() === "a" ? "" : "a", attribute: "href", type: "string" });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const sampleItems = siblings.slice(0, 3).map((item) => {
|
|
395
|
+
const rec = {};
|
|
396
|
+
suggestedFields.forEach((sf) => {
|
|
397
|
+
let val = "";
|
|
398
|
+
if (sf.relativeSelector) {
|
|
399
|
+
const sub = item.querySelector(sf.relativeSelector);
|
|
400
|
+
if (sub) {
|
|
401
|
+
val = sf.attribute === "text" ? (sub.innerText?.trim() || sub.textContent?.trim()) : sub.getAttribute(sf.attribute);
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
val = sf.attribute === "text" ? (item.innerText?.trim() || item.textContent?.trim()) : item.getAttribute(sf.attribute);
|
|
405
|
+
}
|
|
406
|
+
rec[sf.key] = val || "";
|
|
407
|
+
});
|
|
408
|
+
return rec;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
repeatingDetection = {
|
|
412
|
+
isRepeating: true,
|
|
413
|
+
containerSelector,
|
|
414
|
+
itemCount: siblings.length,
|
|
415
|
+
suggestedFields,
|
|
416
|
+
sampleItems,
|
|
417
|
+
};
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
currR = currR.parentElement;
|
|
422
|
+
depth++;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
290
426
|
return {
|
|
291
427
|
found: true,
|
|
292
428
|
tagName: el.tagName.toLowerCase(),
|
|
@@ -298,6 +434,8 @@ export async function handleInspectElement(payload) {
|
|
|
298
434
|
candidates,
|
|
299
435
|
hierarchy,
|
|
300
436
|
boundingRect,
|
|
437
|
+
tableDetection,
|
|
438
|
+
repeatingDetection,
|
|
301
439
|
};
|
|
302
440
|
}, x, y);
|
|
303
441
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import * as cheerio from "cheerio";
|
|
3
3
|
import localBrowserManager from "../browser.js";
|
|
4
|
+
import { extractSmartCheerioText, runTransformPipeline, castValueToType } from "./transformUtils.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Extracts a single field value from a Puppeteer page given a strategy.
|
|
@@ -100,7 +101,7 @@ function extractCheerioField($, strategy) {
|
|
|
100
101
|
const el = $(strategy.value);
|
|
101
102
|
if (!el || el.length === 0) return null;
|
|
102
103
|
|
|
103
|
-
if (attr === "text") return el.first()
|
|
104
|
+
if (attr === "text") return extractSmartCheerioText($, el.first());
|
|
104
105
|
if (attr === "html") return el.first().html();
|
|
105
106
|
return el.first().attr(attr) || null;
|
|
106
107
|
}
|
|
@@ -116,7 +117,7 @@ function extractCheerioField($, strategy) {
|
|
|
116
117
|
try {
|
|
117
118
|
const el = $(css);
|
|
118
119
|
if (el && el.length > 0) {
|
|
119
|
-
if (attr === "text") return el.first()
|
|
120
|
+
if (attr === "text") return extractSmartCheerioText($, el.first());
|
|
120
121
|
if (attr === "html") return el.first().html();
|
|
121
122
|
return el.first().attr(attr) || null;
|
|
122
123
|
}
|
|
@@ -129,7 +130,7 @@ function extractCheerioField($, strategy) {
|
|
|
129
130
|
if (type === "text-relative") {
|
|
130
131
|
const anchor = strategy.anchor || strategy.value;
|
|
131
132
|
const match = $(`:contains("${anchor}")`).last();
|
|
132
|
-
if (match && match.length > 0) return match
|
|
133
|
+
if (match && match.length > 0) return extractSmartCheerioText($, match);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
return null;
|
|
@@ -138,6 +139,181 @@ function extractCheerioField($, strategy) {
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Extracts a table collection using Cheerio.
|
|
144
|
+
*/
|
|
145
|
+
function extractTableCollectionCheerio($, collectionDef) {
|
|
146
|
+
if (!collectionDef) return [];
|
|
147
|
+
const selector = collectionDef.containerSelector || collectionDef.tableSelector || "table";
|
|
148
|
+
const tableEl = $(selector).first();
|
|
149
|
+
if (!tableEl || tableEl.length === 0) return [];
|
|
150
|
+
|
|
151
|
+
const hasHeaders = collectionDef.hasHeaders !== false;
|
|
152
|
+
const headerRowIndex = typeof collectionDef.headerRowIndex === "number" ? collectionDef.headerRowIndex : 0;
|
|
153
|
+
const maxItems = collectionDef.maxItems || 100;
|
|
154
|
+
const columns = collectionDef.columns || [];
|
|
155
|
+
|
|
156
|
+
const rows = [];
|
|
157
|
+
tableEl.find("tr").each((rIdx, rowEl) => {
|
|
158
|
+
const $row = $(rowEl);
|
|
159
|
+
const cells = [];
|
|
160
|
+
$row.find("th, td").each((_, cellEl) => {
|
|
161
|
+
cells.push(extractSmartCheerioText($, $(cellEl)));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (cells.length > 0) {
|
|
165
|
+
rows.push({ rowIndex: rIdx, cells, rowEl: $row });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (rows.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
let activeColumns = [...columns];
|
|
172
|
+
if (activeColumns.length === 0) {
|
|
173
|
+
if (hasHeaders && rows[headerRowIndex]) {
|
|
174
|
+
activeColumns = rows[headerRowIndex].cells.map((headerText, colIdx) => {
|
|
175
|
+
const normalizedKey =
|
|
176
|
+
headerText
|
|
177
|
+
.toLowerCase()
|
|
178
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
179
|
+
.replace(/^_+|_+$/g, "") || `column_${colIdx + 1}`;
|
|
180
|
+
return {
|
|
181
|
+
key: normalizedKey,
|
|
182
|
+
label: headerText || `Column ${colIdx + 1}`,
|
|
183
|
+
columnIndex: colIdx,
|
|
184
|
+
type: "string",
|
|
185
|
+
excluded: false,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
activeColumns = rows[0].cells.map((_, colIdx) => ({
|
|
190
|
+
key: `column_${colIdx + 1}`,
|
|
191
|
+
label: `Column ${colIdx + 1}`,
|
|
192
|
+
columnIndex: colIdx,
|
|
193
|
+
type: "string",
|
|
194
|
+
excluded: false,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const startRowIdx = hasHeaders ? headerRowIndex + 1 : 0;
|
|
200
|
+
const dataRows = rows.slice(startRowIdx, startRowIdx + maxItems);
|
|
201
|
+
const results = [];
|
|
202
|
+
|
|
203
|
+
for (const { cells, rowEl } of dataRows) {
|
|
204
|
+
const record = {};
|
|
205
|
+
for (let colIdx = 0; colIdx < activeColumns.length; colIdx++) {
|
|
206
|
+
const col = activeColumns[colIdx];
|
|
207
|
+
if (col.excluded) continue;
|
|
208
|
+
let rawVal = null;
|
|
209
|
+
const colSelector = col.relativeSelector || col.selector;
|
|
210
|
+
|
|
211
|
+
if (colSelector) {
|
|
212
|
+
const subEl = rowEl.find(colSelector).first();
|
|
213
|
+
if (subEl && subEl.length > 0) {
|
|
214
|
+
const attr = col.attribute || "text";
|
|
215
|
+
rawVal = attr === "text" ? extractSmartCheerioText($, subEl) : subEl.attr(attr) || null;
|
|
216
|
+
}
|
|
217
|
+
} else if (typeof col.columnIndex === "number" && col.columnIndex < cells.length) {
|
|
218
|
+
rawVal = cells[col.columnIndex];
|
|
219
|
+
} else if (colIdx < cells.length) {
|
|
220
|
+
rawVal = cells[colIdx];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if ((rawVal === null || rawVal === undefined || rawVal === "") && col.defaultValue !== undefined) {
|
|
224
|
+
rawVal = col.defaultValue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Apply column transforms
|
|
228
|
+
if (col.transforms && col.transforms.length > 0) {
|
|
229
|
+
rawVal = runTransformPipeline(rawVal, col.transforms);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Cast to target type
|
|
233
|
+
if (col.type) {
|
|
234
|
+
rawVal = castValueToType(rawVal, col.type);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
record[col.key] = rawVal;
|
|
238
|
+
}
|
|
239
|
+
results.push(record);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Extracts repeating items/cards using Cheerio.
|
|
247
|
+
*/
|
|
248
|
+
function extractRepeatingCollectionCheerio($, collectionDef) {
|
|
249
|
+
if (!collectionDef) return [];
|
|
250
|
+
|
|
251
|
+
let items;
|
|
252
|
+
if (collectionDef.rowSelector) {
|
|
253
|
+
if (collectionDef.containerSelector) {
|
|
254
|
+
items = $(collectionDef.containerSelector).find(collectionDef.rowSelector);
|
|
255
|
+
}
|
|
256
|
+
if (!items || items.length === 0) {
|
|
257
|
+
items = $(collectionDef.rowSelector);
|
|
258
|
+
}
|
|
259
|
+
} else if (collectionDef.containerSelector) {
|
|
260
|
+
items = $(collectionDef.containerSelector);
|
|
261
|
+
} else {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!items || items.length === 0) return [];
|
|
266
|
+
|
|
267
|
+
const maxItems = collectionDef.maxItems || 100;
|
|
268
|
+
const columns = collectionDef.columns || [];
|
|
269
|
+
const results = [];
|
|
270
|
+
|
|
271
|
+
items.slice(0, maxItems).each((_, itemEl) => {
|
|
272
|
+
const $item = $(itemEl);
|
|
273
|
+
const record = {};
|
|
274
|
+
|
|
275
|
+
for (const col of columns) {
|
|
276
|
+
if (col.excluded) continue;
|
|
277
|
+
let rawVal = null;
|
|
278
|
+
const colSelector = col.relativeSelector || col.selector;
|
|
279
|
+
|
|
280
|
+
if (colSelector) {
|
|
281
|
+
const target = $item.find(colSelector).first();
|
|
282
|
+
if (target && target.length > 0) {
|
|
283
|
+
const attr = col.attribute || "text";
|
|
284
|
+
rawVal = attr === "text" ? extractSmartCheerioText($, target) : target.attr(attr) || null;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const attr = col.attribute || "text";
|
|
288
|
+
rawVal = attr === "text" ? extractSmartCheerioText($, $item) : $item.attr(attr) || null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if ((rawVal === null || rawVal === undefined || rawVal === "") && col.defaultValue !== undefined) {
|
|
292
|
+
rawVal = col.defaultValue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Apply column transforms
|
|
296
|
+
if (col.transforms && col.transforms.length > 0) {
|
|
297
|
+
rawVal = runTransformPipeline(rawVal, col.transforms);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Cast to target type
|
|
301
|
+
if (col.type) {
|
|
302
|
+
rawVal = castValueToType(rawVal, col.type);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
record[col.key] = rawVal;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const hasValue = Object.values(record).some((v) => v !== null && v !== undefined && v !== "");
|
|
309
|
+
if (hasValue) {
|
|
310
|
+
results.push(record);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
141
317
|
/**
|
|
142
318
|
* Handles SCRAPE_CHROMIUM task via local stealth Puppeteer.
|
|
143
319
|
*/
|
|
@@ -146,6 +322,7 @@ export async function handleChromiumScrape(payload) {
|
|
|
146
322
|
const {
|
|
147
323
|
url,
|
|
148
324
|
fieldMappings = [],
|
|
325
|
+
collections = [],
|
|
149
326
|
navigation = {},
|
|
150
327
|
waitFor = {},
|
|
151
328
|
timeout = 30000,
|
|
@@ -257,6 +434,13 @@ export async function handleChromiumScrape(payload) {
|
|
|
257
434
|
}
|
|
258
435
|
}
|
|
259
436
|
|
|
437
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
438
|
+
extractedVal = runTransformPipeline(extractedVal, mapping.transforms);
|
|
439
|
+
}
|
|
440
|
+
if (mapping.type) {
|
|
441
|
+
extractedVal = castValueToType(extractedVal, mapping.type);
|
|
442
|
+
}
|
|
443
|
+
|
|
260
444
|
raw[mapping.field] = extractedVal;
|
|
261
445
|
strategySummary[mapping.field] = {
|
|
262
446
|
extracted: extractedVal !== null,
|
|
@@ -302,6 +486,163 @@ export async function handleChromiumScrape(payload) {
|
|
|
302
486
|
}
|
|
303
487
|
}
|
|
304
488
|
|
|
489
|
+
// 5. Extract structured collections (Tables & Repeating items)
|
|
490
|
+
const extractedCollections = {};
|
|
491
|
+
if (Array.isArray(collections) && collections.length > 0) {
|
|
492
|
+
for (const coll of collections) {
|
|
493
|
+
if (!coll || !coll.name) continue;
|
|
494
|
+
try {
|
|
495
|
+
if (coll.type === "TABLE") {
|
|
496
|
+
extractedCollections[coll.name] = await page.evaluate((cDef) => {
|
|
497
|
+
const sel = cDef.containerSelector || "table";
|
|
498
|
+
const table = document.querySelector(sel);
|
|
499
|
+
if (!table) return [];
|
|
500
|
+
|
|
501
|
+
const hasHeaders = cDef.hasHeaders !== false;
|
|
502
|
+
const headerRowIndex = typeof cDef.headerRowIndex === "number" ? cDef.headerRowIndex : 0;
|
|
503
|
+
const maxItems = cDef.maxItems || 100;
|
|
504
|
+
const columns = cDef.columns || [];
|
|
505
|
+
|
|
506
|
+
const rowEls = Array.from(table.querySelectorAll("tr"));
|
|
507
|
+
const rowsData = rowEls
|
|
508
|
+
.map((r) => {
|
|
509
|
+
const cells = Array.from(r.querySelectorAll("th, td")).map(
|
|
510
|
+
(c) => c.innerText?.trim() || c.textContent?.trim() || ""
|
|
511
|
+
);
|
|
512
|
+
return { cells, el: r };
|
|
513
|
+
})
|
|
514
|
+
.filter((r) => r.cells.length > 0);
|
|
515
|
+
|
|
516
|
+
if (rowsData.length === 0) return [];
|
|
517
|
+
|
|
518
|
+
let activeColumns = [...columns];
|
|
519
|
+
if (activeColumns.length === 0) {
|
|
520
|
+
if (hasHeaders && rowsData[headerRowIndex]) {
|
|
521
|
+
activeColumns = rowsData[headerRowIndex].cells.map((h, idx) => {
|
|
522
|
+
const key =
|
|
523
|
+
h.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") ||
|
|
524
|
+
`col_${idx + 1}`;
|
|
525
|
+
return { key, label: h || `Column ${idx + 1}`, columnIndex: idx, excluded: false };
|
|
526
|
+
});
|
|
527
|
+
} else {
|
|
528
|
+
activeColumns = rowsData[0].cells.map((_, idx) => ({
|
|
529
|
+
key: `col_${idx + 1}`,
|
|
530
|
+
label: `Column ${idx + 1}`,
|
|
531
|
+
columnIndex: idx,
|
|
532
|
+
excluded: false,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const startIdx = hasHeaders ? headerRowIndex + 1 : 0;
|
|
538
|
+
const sliceRows = rowsData.slice(startIdx, startIdx + maxItems);
|
|
539
|
+
const results = [];
|
|
540
|
+
|
|
541
|
+
for (const row of sliceRows) {
|
|
542
|
+
const rec = {};
|
|
543
|
+
for (let colIdx = 0; colIdx < activeColumns.length; colIdx++) {
|
|
544
|
+
const col = activeColumns[colIdx];
|
|
545
|
+
if (col.excluded) continue;
|
|
546
|
+
let val = null;
|
|
547
|
+
const colSel = col.relativeSelector || col.selector;
|
|
548
|
+
if (colSel) {
|
|
549
|
+
const subEl = row.el.querySelector(colSel);
|
|
550
|
+
if (subEl) {
|
|
551
|
+
const attr = col.attribute || "text";
|
|
552
|
+
val = attr === "text" ? (subEl.innerText?.trim() || subEl.textContent?.trim()) : subEl.getAttribute(attr);
|
|
553
|
+
}
|
|
554
|
+
} else if (typeof col.columnIndex === "number" && col.columnIndex < row.cells.length) {
|
|
555
|
+
val = row.cells[col.columnIndex];
|
|
556
|
+
} else if (colIdx < row.cells.length) {
|
|
557
|
+
val = row.cells[colIdx];
|
|
558
|
+
}
|
|
559
|
+
if ((val === null || val === undefined || val === "") && col.defaultValue !== undefined) {
|
|
560
|
+
val = col.defaultValue;
|
|
561
|
+
}
|
|
562
|
+
rec[col.key] = val;
|
|
563
|
+
}
|
|
564
|
+
results.push(rec);
|
|
565
|
+
}
|
|
566
|
+
return results;
|
|
567
|
+
}, coll);
|
|
568
|
+
} else {
|
|
569
|
+
extractedCollections[coll.name] = await page.evaluate((cDef) => {
|
|
570
|
+
let items = [];
|
|
571
|
+
if (cDef.rowSelector) {
|
|
572
|
+
if (cDef.containerSelector) {
|
|
573
|
+
const container = document.querySelector(cDef.containerSelector);
|
|
574
|
+
if (container) {
|
|
575
|
+
items = Array.from(container.querySelectorAll(cDef.rowSelector));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (items.length === 0) {
|
|
579
|
+
items = Array.from(document.querySelectorAll(cDef.rowSelector));
|
|
580
|
+
}
|
|
581
|
+
} else if (cDef.containerSelector) {
|
|
582
|
+
items = Array.from(document.querySelectorAll(cDef.containerSelector));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (items.length === 0) return [];
|
|
586
|
+
|
|
587
|
+
const maxItems = cDef.maxItems || 100;
|
|
588
|
+
const columns = cDef.columns || [];
|
|
589
|
+
const results = [];
|
|
590
|
+
|
|
591
|
+
for (const item of items.slice(0, maxItems)) {
|
|
592
|
+
const rec = {};
|
|
593
|
+
for (const col of columns) {
|
|
594
|
+
if (col.excluded) continue;
|
|
595
|
+
let val = null;
|
|
596
|
+
const colSel = col.relativeSelector || col.selector;
|
|
597
|
+
if (colSel) {
|
|
598
|
+
const target = item.querySelector(colSel);
|
|
599
|
+
if (target) {
|
|
600
|
+
const attr = col.attribute || "text";
|
|
601
|
+
val = attr === "text" ? (target.innerText?.trim() || target.textContent?.trim()) : target.getAttribute(attr);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
const attr = col.attribute || "text";
|
|
605
|
+
val = attr === "text" ? (item.innerText?.trim() || item.textContent?.trim()) : item.getAttribute(attr);
|
|
606
|
+
}
|
|
607
|
+
if ((val === null || val === undefined || val === "") && col.defaultValue !== undefined) {
|
|
608
|
+
val = col.defaultValue;
|
|
609
|
+
}
|
|
610
|
+
rec[col.key] = val;
|
|
611
|
+
}
|
|
612
|
+
const hasVal = Object.values(rec).some((v) => v !== null && v !== undefined && v !== "");
|
|
613
|
+
if (hasVal) results.push(rec);
|
|
614
|
+
}
|
|
615
|
+
return results;
|
|
616
|
+
}, coll);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Apply column transforms and type casting
|
|
620
|
+
if (Array.isArray(extractedCollections[coll.name]) && coll.columns) {
|
|
621
|
+
extractedCollections[coll.name] = extractedCollections[coll.name].map((rec) => {
|
|
622
|
+
const transformed = { ...rec };
|
|
623
|
+
for (const col of coll.columns) {
|
|
624
|
+
if (col.excluded) continue;
|
|
625
|
+
let val = transformed[col.key];
|
|
626
|
+
if ((val === null || val === undefined || val === "") && col.defaultValue !== undefined) {
|
|
627
|
+
val = col.defaultValue;
|
|
628
|
+
}
|
|
629
|
+
if (col.transforms && col.transforms.length > 0) {
|
|
630
|
+
val = runTransformPipeline(val, col.transforms);
|
|
631
|
+
}
|
|
632
|
+
if (col.type) {
|
|
633
|
+
val = castValueToType(val, col.type);
|
|
634
|
+
}
|
|
635
|
+
transformed[col.key] = val;
|
|
636
|
+
}
|
|
637
|
+
return transformed;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
extractedCollections[coll.name] = [];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
305
646
|
const executionTimeMs = Date.now() - startTime;
|
|
306
647
|
const hasExtractedValues = Object.values(raw).some(
|
|
307
648
|
(v) => v !== null && v !== undefined && v !== ""
|
|
@@ -320,6 +661,7 @@ export async function handleChromiumScrape(payload) {
|
|
|
320
661
|
raw,
|
|
321
662
|
strategySummary,
|
|
322
663
|
extractedData: raw,
|
|
664
|
+
collections: extractedCollections,
|
|
323
665
|
html,
|
|
324
666
|
executionTimeMs,
|
|
325
667
|
};
|
|
@@ -336,6 +678,7 @@ export async function handleHttpScrape(payload) {
|
|
|
336
678
|
const {
|
|
337
679
|
url,
|
|
338
680
|
fieldMappings = [],
|
|
681
|
+
collections = [],
|
|
339
682
|
timeout = 15000,
|
|
340
683
|
headers = {},
|
|
341
684
|
extractSelectors = [],
|
|
@@ -353,7 +696,7 @@ export async function handleHttpScrape(payload) {
|
|
|
353
696
|
},
|
|
354
697
|
});
|
|
355
698
|
|
|
356
|
-
|
|
699
|
+
let pageHtml = typeof response.data === "string" ? response.data : Buffer.isBuffer(response.data) ? response.data.toString("utf-8") : "";
|
|
357
700
|
const $ = cheerio.load(pageHtml);
|
|
358
701
|
const title = $("title").first().text().trim() || "";
|
|
359
702
|
|
|
@@ -377,6 +720,13 @@ export async function handleHttpScrape(payload) {
|
|
|
377
720
|
}
|
|
378
721
|
}
|
|
379
722
|
|
|
723
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
724
|
+
extractedVal = runTransformPipeline(extractedVal, mapping.transforms);
|
|
725
|
+
}
|
|
726
|
+
if (mapping.type) {
|
|
727
|
+
extractedVal = castValueToType(extractedVal, mapping.type);
|
|
728
|
+
}
|
|
729
|
+
|
|
380
730
|
raw[mapping.field] = extractedVal;
|
|
381
731
|
strategySummary[mapping.field] = {
|
|
382
732
|
extracted: extractedVal !== null,
|
|
@@ -415,6 +765,23 @@ export async function handleHttpScrape(payload) {
|
|
|
415
765
|
}
|
|
416
766
|
}
|
|
417
767
|
|
|
768
|
+
// 3. Extract structured collections (Tables & Repeating items)
|
|
769
|
+
const extractedCollections = {};
|
|
770
|
+
if (Array.isArray(collections) && collections.length > 0) {
|
|
771
|
+
for (const coll of collections) {
|
|
772
|
+
if (!coll || !coll.name) continue;
|
|
773
|
+
try {
|
|
774
|
+
if (coll.type === "TABLE") {
|
|
775
|
+
extractedCollections[coll.name] = extractTableCollectionCheerio($, coll);
|
|
776
|
+
} else {
|
|
777
|
+
extractedCollections[coll.name] = extractRepeatingCollectionCheerio($, coll);
|
|
778
|
+
}
|
|
779
|
+
} catch {
|
|
780
|
+
extractedCollections[coll.name] = [];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
418
785
|
const executionTimeMs = Date.now() - startTime;
|
|
419
786
|
const hasExtractedValues = Object.values(raw).some(
|
|
420
787
|
(v) => v !== null && v !== undefined && v !== ""
|
|
@@ -427,6 +794,7 @@ export async function handleHttpScrape(payload) {
|
|
|
427
794
|
raw,
|
|
428
795
|
strategySummary,
|
|
429
796
|
extractedData: raw,
|
|
797
|
+
collections: extractedCollections,
|
|
430
798
|
html: includeHtml || (!hasExtractedValues && fieldMappings.length > 0) ? pageHtml : "",
|
|
431
799
|
executionTimeMs,
|
|
432
800
|
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformation and text extraction utilities for the Edge Worker.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safely extracts text from Cheerio elements while preserving block/boundary line breaks (\n).
|
|
7
|
+
* Prevents glued text like "678.0013.30 (2.00%)" caused by native $(el).text() concatenation.
|
|
8
|
+
*
|
|
9
|
+
* @param {cheerio.CheerioAPI} $
|
|
10
|
+
* @param {cheerio.Cheerio} el
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function extractSmartCheerioText($, el) {
|
|
14
|
+
if (!el || el.length === 0) return "";
|
|
15
|
+
try {
|
|
16
|
+
const clone = $(el).clone();
|
|
17
|
+
clone.find("br").replaceWith("\n");
|
|
18
|
+
clone.find("div, p, span, small, button, li, h1, h2, h3, h4, h5, h6, tr, td, th, section, article, header, footer, nav, aside, dt, dd").before("\n").after("\n");
|
|
19
|
+
return clone
|
|
20
|
+
.text()
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map((s) => s.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join("\n");
|
|
25
|
+
} catch {
|
|
26
|
+
return $(el).text().trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Executes a single transformation step safely on an input value.
|
|
32
|
+
*
|
|
33
|
+
* @param {*} value
|
|
34
|
+
* @param {Object} step
|
|
35
|
+
* @returns {*}
|
|
36
|
+
*/
|
|
37
|
+
export function applyTransformStep(value, step) {
|
|
38
|
+
if (!step) return value;
|
|
39
|
+
|
|
40
|
+
const op = (step.operation || step.op || "").toLowerCase();
|
|
41
|
+
if (!op) return value;
|
|
42
|
+
|
|
43
|
+
if (op === "default") {
|
|
44
|
+
if (value === null || value === undefined || value === "") {
|
|
45
|
+
const defVal = step.value !== undefined ? step.value : step.params?.value;
|
|
46
|
+
return defVal !== undefined ? defVal : null;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
switch (op) {
|
|
56
|
+
case "trim": {
|
|
57
|
+
return typeof value === "string" ? value.trim() : String(value).trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "lowercase": {
|
|
61
|
+
return typeof value === "string" ? value.toLowerCase() : String(value).toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "uppercase": {
|
|
65
|
+
return typeof value === "string" ? value.toUpperCase() : String(value).toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "remove": {
|
|
69
|
+
const toRemove = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
|
|
70
|
+
if (!toRemove) return value;
|
|
71
|
+
return String(value).split(toRemove).join("");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "remove_prefix": {
|
|
75
|
+
const prefix = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
|
|
76
|
+
if (!prefix) return value;
|
|
77
|
+
const strVal = String(value).trim();
|
|
78
|
+
if (strVal.startsWith(prefix)) {
|
|
79
|
+
return strVal.slice(prefix.length).trim();
|
|
80
|
+
}
|
|
81
|
+
return strVal;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "remove_suffix": {
|
|
85
|
+
const suffix = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
|
|
86
|
+
if (!suffix) return value;
|
|
87
|
+
const strVal = String(value).trim();
|
|
88
|
+
if (strVal.endsWith(suffix)) {
|
|
89
|
+
return strVal.slice(0, strVal.length - suffix.length).trim();
|
|
90
|
+
}
|
|
91
|
+
return strVal;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "replace": {
|
|
95
|
+
const fromStr = step.from !== undefined ? String(step.from) : step.params?.from !== undefined ? String(step.params.from) : "";
|
|
96
|
+
const toStr = step.to !== undefined ? String(step.to) : step.params?.to !== undefined ? String(step.params.to) : "";
|
|
97
|
+
return String(value).split(fromStr).join(toStr);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "regex_extract": {
|
|
101
|
+
const pattern = step.pattern || step.params?.pattern;
|
|
102
|
+
if (!pattern) return value;
|
|
103
|
+
try {
|
|
104
|
+
const regex = new RegExp(pattern);
|
|
105
|
+
const match = String(value).match(regex);
|
|
106
|
+
if (!match) return null;
|
|
107
|
+
return match[1] !== undefined ? match[1] : match[0];
|
|
108
|
+
} catch {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case "regex_replace": {
|
|
114
|
+
const pattern = step.pattern || step.params?.pattern;
|
|
115
|
+
if (!pattern) return value;
|
|
116
|
+
try {
|
|
117
|
+
const regex = new RegExp(pattern, "g");
|
|
118
|
+
const toStr = step.to !== undefined ? String(step.to) : step.params?.to !== undefined ? String(step.params.to) : "";
|
|
119
|
+
return String(value).replace(regex, toStr);
|
|
120
|
+
} catch {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "split": {
|
|
126
|
+
let sep = step.separator !== undefined ? String(step.separator) : step.params?.separator !== undefined ? String(step.params.separator) : step.params?.delimiter !== undefined ? String(step.params.delimiter) : step.value !== undefined ? String(step.value) : "\n";
|
|
127
|
+
if (sep === "\\n") sep = "\n";
|
|
128
|
+
else if (sep === "\\t") sep = "\t";
|
|
129
|
+
const parts = String(value).split(sep).map((s) => s.trim());
|
|
130
|
+
const index = step.index !== undefined ? step.index : step.params?.index;
|
|
131
|
+
if (index !== undefined && index !== null) {
|
|
132
|
+
const idx = parseInt(index, 10);
|
|
133
|
+
if (isNaN(idx)) return null;
|
|
134
|
+
const targetIdx = idx < 0 ? parts.length + idx : idx;
|
|
135
|
+
return targetIdx >= 0 && targetIdx < parts.length ? parts[targetIdx] : null;
|
|
136
|
+
}
|
|
137
|
+
return parts;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case "first_line": {
|
|
141
|
+
const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
142
|
+
return parts.length > 0 ? parts[0] : "";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case "second_line": {
|
|
146
|
+
const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
147
|
+
return parts.length > 1 ? parts[1] : "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "line": {
|
|
151
|
+
const idx = parseInt(step.index ?? step.value ?? step.params?.index ?? step.params?.value ?? 0, 10);
|
|
152
|
+
const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
153
|
+
if (isNaN(idx)) return parts[0] || "";
|
|
154
|
+
const targetIdx = idx < 0 ? parts.length + idx : idx;
|
|
155
|
+
return targetIdx >= 0 && targetIdx < parts.length ? parts[targetIdx] : "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case "contains":
|
|
159
|
+
case "includes":
|
|
160
|
+
case "has_badge": {
|
|
161
|
+
const searchStr = step.value !== undefined ? String(step.value) : step.pattern !== undefined ? String(step.pattern) : step.params?.badgeKeyword !== undefined ? String(step.params.badgeKeyword) : step.params?.value !== undefined ? String(step.params.value) : "";
|
|
162
|
+
if (!searchStr) return false;
|
|
163
|
+
const caseSensitive = step.caseSensitive === true || step.params?.caseSensitive === true;
|
|
164
|
+
const strVal = String(value);
|
|
165
|
+
return caseSensitive
|
|
166
|
+
? strVal.includes(searchStr)
|
|
167
|
+
: strVal.toLowerCase().includes(searchStr.toLowerCase());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "cast": {
|
|
171
|
+
const targetType = (step.targetType || step.params?.targetType || step.type || step.value || "string").toLowerCase();
|
|
172
|
+
return castValueToType(value, targetType);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "join": {
|
|
176
|
+
const delimiter = step.delimiter !== undefined ? String(step.delimiter) : step.value !== undefined ? String(step.value) : ", ";
|
|
177
|
+
return Array.isArray(value) ? value.join(delimiter) : String(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "currency_cleanup": {
|
|
181
|
+
if (typeof value === "number") return value;
|
|
182
|
+
const str = String(value).trim();
|
|
183
|
+
const cleaned = str
|
|
184
|
+
.replace(/[\u20B9$€£¥₹]/g, "")
|
|
185
|
+
.replace(/\b(USD|EUR|GBP|INR|CAD|AUD|JPY|CHF|AED|Rs\.?|Rupees?)\b/gi, "")
|
|
186
|
+
.replace(/\s+/g, " ")
|
|
187
|
+
.replace(/,/g, "")
|
|
188
|
+
.trim();
|
|
189
|
+
const numMatch = cleaned.match(/[-+]?[0-9]+(?:\.[0-9]+)?/);
|
|
190
|
+
return numMatch ? parseFloat(numMatch[0]) : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case "number":
|
|
194
|
+
case "decimal": {
|
|
195
|
+
if (typeof value === "number") return value;
|
|
196
|
+
const str = String(value).trim().replace(/,/g, "");
|
|
197
|
+
const numMatch = str.match(/[-+]?[0-9]+(?:\.[0-9]+)?/);
|
|
198
|
+
return numMatch ? parseFloat(numMatch[0]) : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case "integer": {
|
|
202
|
+
if (typeof value === "number") return Math.round(value);
|
|
203
|
+
const str = String(value).trim().replace(/,/g, "");
|
|
204
|
+
const numMatch = str.match(/[-+]?[0-9]+/);
|
|
205
|
+
return numMatch ? parseInt(numMatch[0], 10) : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case "parse_boolean":
|
|
209
|
+
case "boolean": {
|
|
210
|
+
if (typeof value === "boolean") return value;
|
|
211
|
+
const str = String(value).trim().toLowerCase();
|
|
212
|
+
if (["true", "yes", "y", "1", "in stock", "available", "on", "active"].includes(str)) return true;
|
|
213
|
+
if (["false", "no", "n", "0", "out of stock", "unavailable", "off", "inactive"].includes(str)) return false;
|
|
214
|
+
return Boolean(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case "has_value":
|
|
218
|
+
case "is_not_empty":
|
|
219
|
+
case "not_empty":
|
|
220
|
+
case "exists": {
|
|
221
|
+
if (value === null || value === undefined) return false;
|
|
222
|
+
const lineIdx = step.lineIndex !== undefined ? step.lineIndex : step.params?.lineIndex !== undefined ? step.params?.lineIndex : step.index !== undefined ? step.index : step.params?.index;
|
|
223
|
+
if (lineIdx !== undefined && lineIdx !== null && lineIdx !== "") {
|
|
224
|
+
const idx = parseInt(lineIdx, 10);
|
|
225
|
+
const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
226
|
+
const targetIdx = idx > 0 ? idx - 1 : idx; // 1-indexed line number
|
|
227
|
+
return targetIdx >= 0 && targetIdx < parts.length && parts[targetIdx].length > 0;
|
|
228
|
+
}
|
|
229
|
+
const str = String(value).trim();
|
|
230
|
+
return str.length > 0 && str.toLowerCase() !== "false" && str !== "0";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "custom":
|
|
234
|
+
case "expression":
|
|
235
|
+
case "script": {
|
|
236
|
+
const code = step.code || step.expression || step.value || step.script || step.params?.code || step.params?.expression || step.params?.script;
|
|
237
|
+
if (!code || typeof code !== "string") return value;
|
|
238
|
+
try {
|
|
239
|
+
const trimmed = code.trim();
|
|
240
|
+
if (/\b(process|require|import|global|Buffer|__dirname|__filename|eval|Function)\b/.test(trimmed)) {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Auto-quote unquoted string after == or === or != or !== e.g. == Vol Shocker -> == "Vol Shocker"
|
|
245
|
+
let sanitizedCode = trimmed.replace(/([=!]==?\s*)([A-Za-z][A-Za-z0-9 _-]*[A-Za-z0-9])(?=\s*$|\s*[;)])/g, (match, op, str) => {
|
|
246
|
+
const trimmedStr = str.trim();
|
|
247
|
+
if (['true', 'false', 'null', 'undefined', 'value', 'raw', 'lines'].includes(trimmedStr)) return match;
|
|
248
|
+
if (trimmedStr.startsWith('"') || trimmedStr.startsWith("'")) return match;
|
|
249
|
+
return `${op}"${trimmedStr}"`;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const lines = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
253
|
+
const line = (n) => {
|
|
254
|
+
const idx = parseInt(n, 10);
|
|
255
|
+
const targetIdx = idx > 0 ? idx - 1 : idx;
|
|
256
|
+
return lines[targetIdx] || "";
|
|
257
|
+
};
|
|
258
|
+
const hasLine = (n) => {
|
|
259
|
+
const idx = parseInt(n, 10);
|
|
260
|
+
const targetIdx = idx > 0 ? idx - 1 : idx;
|
|
261
|
+
return targetIdx >= 0 && targetIdx < lines.length && lines[targetIdx].length > 0;
|
|
262
|
+
};
|
|
263
|
+
const has = (k) => String(value).toLowerCase().includes(String(k).toLowerCase());
|
|
264
|
+
const contains = has;
|
|
265
|
+
const hasValue = (v = value) => v !== null && v !== undefined && String(v).trim().length > 0;
|
|
266
|
+
const notEmpty = hasValue;
|
|
267
|
+
|
|
268
|
+
const fn = sanitizedCode.includes("return ")
|
|
269
|
+
? new Function("value", "raw", "lines", "line", "hasLine", "has", "contains", "hasValue", "notEmpty", sanitizedCode)
|
|
270
|
+
: new Function("value", "raw", "lines", "line", "hasLine", "has", "contains", "hasValue", "notEmpty", `return (${sanitizedCode});`);
|
|
271
|
+
const result = fn(value, value, lines, line, hasLine, has, contains, hasValue, notEmpty);
|
|
272
|
+
return result !== undefined ? result : value;
|
|
273
|
+
} catch {
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
default:
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Runs an array of transform steps sequentially on a raw extracted value.
|
|
285
|
+
*
|
|
286
|
+
* @param {*} rawValue
|
|
287
|
+
* @param {Array<Object>} pipeline
|
|
288
|
+
* @returns {*}
|
|
289
|
+
*/
|
|
290
|
+
export function runTransformPipeline(rawValue, pipeline = []) {
|
|
291
|
+
if (!Array.isArray(pipeline) || pipeline.length === 0) {
|
|
292
|
+
return rawValue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let current = rawValue;
|
|
296
|
+
for (const step of pipeline) {
|
|
297
|
+
current = applyTransformStep(current, step);
|
|
298
|
+
}
|
|
299
|
+
return current;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Casts a transformed value to the target data type.
|
|
304
|
+
*
|
|
305
|
+
* @param {*} val
|
|
306
|
+
* @param {string} type
|
|
307
|
+
* @returns {*}
|
|
308
|
+
*/
|
|
309
|
+
export function castValueToType(val, type = "string") {
|
|
310
|
+
if (val === null || val === undefined) return val;
|
|
311
|
+
|
|
312
|
+
switch (type) {
|
|
313
|
+
case "integer": {
|
|
314
|
+
if (typeof val === "number") return Math.round(val);
|
|
315
|
+
const str = String(val).replace(/,/g, "").trim();
|
|
316
|
+
const num = parseInt(str, 10);
|
|
317
|
+
return isNaN(num) ? null : num;
|
|
318
|
+
}
|
|
319
|
+
case "number":
|
|
320
|
+
case "decimal": {
|
|
321
|
+
if (typeof val === "number") return val;
|
|
322
|
+
const str = String(val).replace(/[\u20B9$€£¥₹]/g, "").replace(/,/g, "").trim();
|
|
323
|
+
const num = parseFloat(str);
|
|
324
|
+
return isNaN(num) ? null : num;
|
|
325
|
+
}
|
|
326
|
+
case "boolean": {
|
|
327
|
+
if (typeof val === "boolean") return val;
|
|
328
|
+
const str = String(val).trim().toLowerCase();
|
|
329
|
+
return ["true", "yes", "1", "in stock", "available"].includes(str);
|
|
330
|
+
}
|
|
331
|
+
case "date": {
|
|
332
|
+
if (val instanceof Date) return val.toISOString();
|
|
333
|
+
const parsed = Date.parse(String(val));
|
|
334
|
+
return !isNaN(parsed) ? new Date(parsed).toISOString() : String(val);
|
|
335
|
+
}
|
|
336
|
+
case "string":
|
|
337
|
+
default:
|
|
338
|
+
return typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
339
|
+
}
|
|
340
|
+
}
|