@owox/connectors 0.36.0 → 0.37.0
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/index.cjs +393 -93
- package/dist/index.js +393 -93
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -349,6 +349,7 @@ var require_index = __commonJS({
|
|
|
349
349
|
async run() {
|
|
350
350
|
var _a;
|
|
351
351
|
this.context.emit(new ControlEvent(CONTROL_ACTION.STARTED));
|
|
352
|
+
this._rowsByNode = /* @__PURE__ */ new Map();
|
|
352
353
|
try {
|
|
353
354
|
this.context.validate();
|
|
354
355
|
this._processRunConfig();
|
|
@@ -375,6 +376,7 @@ var require_index = __commonJS({
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
this._reportAccountOutcomes(state);
|
|
379
|
+
this._reportEmptyNodes([...plainNodes, ...timeSeriesNodes]);
|
|
378
380
|
this.source.onImportComplete(this.context);
|
|
379
381
|
this.context.emit(new ControlEvent(CONTROL_ACTION.COMPLETED));
|
|
380
382
|
} catch (error) {
|
|
@@ -510,10 +512,11 @@ var require_index = __commonJS({
|
|
|
510
512
|
* @param {object} state run state from _createRunState
|
|
511
513
|
* @param {object|null} account the account this unit belongs to
|
|
512
514
|
* @param {Function} work async thunk performing the fetch + write
|
|
515
|
+
* @param {string} subject what the unit imports -- a node name, or the day for day-by-day
|
|
513
516
|
* @returns {Promise<boolean>} true when the unit completed
|
|
514
517
|
* @private
|
|
515
518
|
*/
|
|
516
|
-
async _runForAccount(state, account, work) {
|
|
519
|
+
async _runForAccount(state, account, work, subject) {
|
|
517
520
|
try {
|
|
518
521
|
await work();
|
|
519
522
|
state.succeeded.add(this._accountKey(account));
|
|
@@ -521,7 +524,7 @@ var require_index = __commonJS({
|
|
|
521
524
|
} catch (error) {
|
|
522
525
|
this.source.onAccountError(account, error);
|
|
523
526
|
if (error == null ? void 0 : error[STORAGE_FAILURE]) throw error;
|
|
524
|
-
this._recordAccountFailure(state, account, error);
|
|
527
|
+
this._recordAccountFailure(state, account, error, subject);
|
|
525
528
|
return false;
|
|
526
529
|
}
|
|
527
530
|
}
|
|
@@ -560,23 +563,29 @@ var require_index = __commonJS({
|
|
|
560
563
|
* @param {object} state run state from _createRunState
|
|
561
564
|
* @param {object|null} account the account that failed
|
|
562
565
|
* @param {Error} error the failure to classify
|
|
566
|
+
* @param {string} [subject] what was being imported, named in the report as main did
|
|
563
567
|
* @private
|
|
564
568
|
*/
|
|
565
|
-
_recordAccountFailure(state, account, error) {
|
|
569
|
+
_recordAccountFailure(state, account, error, subject) {
|
|
566
570
|
const accountId = this._accountKey(account);
|
|
567
571
|
const isSkip = (error == null ? void 0 : error.isWarning) === true;
|
|
568
572
|
if (!isSkip) state.passHardFailures += 1;
|
|
569
573
|
let entry = state.issues.get(accountId);
|
|
570
574
|
if (!entry) {
|
|
571
|
-
entry = { errors: [], reported: /* @__PURE__ */ new Set() };
|
|
575
|
+
entry = { errors: [], reported: /* @__PURE__ */ new Set(), subjects: /* @__PURE__ */ new Set() };
|
|
572
576
|
state.issues.set(accountId, entry);
|
|
573
577
|
}
|
|
574
578
|
entry.errors.push(error);
|
|
579
|
+
if (subject) entry.subjects.add(subject);
|
|
575
580
|
const level = isSkip ? LOG_LEVEL.WARN : LOG_LEVEL.ERROR;
|
|
576
581
|
if (entry.reported.has(error.message)) return;
|
|
577
582
|
entry.reported.add(error.message);
|
|
578
583
|
const what = account === null || account === void 0 ? isSkip ? "Skipped" : "Import failed" : `${isSkip ? "Skipped account" : "Error processing account"} ${accountId}`;
|
|
579
|
-
|
|
584
|
+
const line = `${what}: ${error.message}`;
|
|
585
|
+
this.context.log(
|
|
586
|
+
level,
|
|
587
|
+
subject ? `Importing ${subject}: ${line[0].toLowerCase()}${line.slice(1)}` : line
|
|
588
|
+
);
|
|
580
589
|
}
|
|
581
590
|
/**
|
|
582
591
|
* Whether the account has failed outright (not merely been skipped) with the same error
|
|
@@ -635,7 +644,48 @@ var require_index = __commonJS({
|
|
|
635
644
|
*/
|
|
636
645
|
_emitCursor(state, date) {
|
|
637
646
|
if (state.cursorHalted) return;
|
|
638
|
-
|
|
647
|
+
const claimed = state.cursorCap && date > state.cursorCap ? state.cursorCap : date;
|
|
648
|
+
if (claimed !== date && claimed === state.lastClaimed) return;
|
|
649
|
+
state.lastClaimed = claimed;
|
|
650
|
+
this.context.emit(new StateEvent({ lastRequestedDate: claimed }));
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* The last day a node is asked for: the run's end, or `endLagDays` before today for an API
|
|
654
|
+
* that reports only completed days, whichever is earlier.
|
|
655
|
+
* @private
|
|
656
|
+
*/
|
|
657
|
+
_nodeWindowEnd(node, dateRange) {
|
|
658
|
+
var _a, _b;
|
|
659
|
+
const lag = ((_b = (_a = this.source).getEndLagDays) == null ? void 0 : _b.call(_a, node.name)) ?? 0;
|
|
660
|
+
if (!lag) return dateRange.endDate;
|
|
661
|
+
const today = this._parseDate(this._formatDate(/* @__PURE__ */ new Date()));
|
|
662
|
+
const lagged = this._formatDate(new Date(today - lag * 864e5));
|
|
663
|
+
return lagged < dateRange.endDate ? lagged : dateRange.endDate;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Notes where `endLagDays` stopped a manual backfill short of its EndDate. The run completes
|
|
667
|
+
* either way, and nothing else on it names the days it left out.
|
|
668
|
+
* @private
|
|
669
|
+
*/
|
|
670
|
+
_logLagCut(node, dateRange, end) {
|
|
671
|
+
var _a;
|
|
672
|
+
if (((_a = this.context.runConfig) == null ? void 0 : _a.type) !== RUN_CONFIG_TYPE.MANUAL_BACKFILL) return;
|
|
673
|
+
if (end >= dateRange.endDate) return;
|
|
674
|
+
this.context.log(
|
|
675
|
+
LOG_LEVEL.INFO,
|
|
676
|
+
`Node "${node.name}" stops at ${end}, not at the EndDate ${dateRange.endDate}: endLagDays ends its window that many days before today`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Notes a node that has nothing to ask for this run because its lag ends its window
|
|
681
|
+
* before the run's first day.
|
|
682
|
+
* @private
|
|
683
|
+
*/
|
|
684
|
+
_logLaggedOut(node, dateRange, end) {
|
|
685
|
+
this.context.log(
|
|
686
|
+
LOG_LEVEL.INFO,
|
|
687
|
+
`Node "${node.name}" asks for nothing this run: endLagDays ends its window on ${end}, before the run's first day ${dateRange.startDate}`
|
|
688
|
+
);
|
|
639
689
|
}
|
|
640
690
|
/**
|
|
641
691
|
* Lists each account's latest error; for a source without accounts, just the error.
|
|
@@ -725,7 +775,9 @@ var require_index = __commonJS({
|
|
|
725
775
|
} else if (state.accountless) {
|
|
726
776
|
message = `Nothing was imported because access was refused: ${describe(entries)}`;
|
|
727
777
|
} else {
|
|
728
|
-
|
|
778
|
+
const subjects = new Set(entries.flatMap(([, entry]) => [...entry.subjects]));
|
|
779
|
+
const where = subjects.size === 1 ? ` while importing ${[...subjects][0]}` : "";
|
|
780
|
+
message = `All ${state.attemptedCount} accounts were skipped${where}, so nothing was imported. This points to a global failure, such as an expired access token, rather than individual accounts being inaccessible. Errors: ${describe(entries)}`;
|
|
729
781
|
}
|
|
730
782
|
const error = new Error(message);
|
|
731
783
|
error.isWarning = failed.length === 0;
|
|
@@ -808,11 +860,13 @@ var require_index = __commonJS({
|
|
|
808
860
|
} catch (error) {
|
|
809
861
|
throw asStorageFailure(error);
|
|
810
862
|
}
|
|
863
|
+
this._countRows(writer.nodeName, rows.length);
|
|
811
864
|
}
|
|
812
865
|
/**
|
|
813
866
|
* Resolves short links for the fields a schema node declares under `shortLinks`, writing
|
|
814
|
-
* each
|
|
815
|
-
* Links is off, or no spec has its field (and its `_parsed`
|
|
867
|
+
* the address each short link service points to next to the original. No-op when the node
|
|
868
|
+
* has no spec, Process Short Links is off, or no spec has its field (and its `_parsed`
|
|
869
|
+
* target) selected.
|
|
816
870
|
*
|
|
817
871
|
* The helpers are bare globals of Core/Utils/ShortLinksUtils.js, a script in the bundle's
|
|
818
872
|
* scope; nothing here touches them for a node without a spec.
|
|
@@ -823,7 +877,7 @@ var require_index = __commonJS({
|
|
|
823
877
|
* @returns {Promise<object[]>} records with resolved links, or the same array
|
|
824
878
|
*/
|
|
825
879
|
async resolveShortLinks(nodeName, data, fields) {
|
|
826
|
-
var _a, _b, _c, _d
|
|
880
|
+
var _a, _b, _c, _d;
|
|
827
881
|
const specs = (_c = (_b = (_a = this.source) == null ? void 0 : _a.fieldsSchema) == null ? void 0 : _b[nodeName]) == null ? void 0 : _c.shortLinks;
|
|
828
882
|
if (!Array.isArray(specs) || specs.length === 0 || !data.length) return data;
|
|
829
883
|
if (((_d = this.context.getParameter("ProcessShortLinks")) == null ? void 0 : _d.value) === false) return data;
|
|
@@ -834,12 +888,7 @@ var require_index = __commonJS({
|
|
|
834
888
|
if (activeSpecs.length === 0) return data;
|
|
835
889
|
const cache = this._shortLinksCache();
|
|
836
890
|
return resolveShortLinkFields(data, activeSpecs, {
|
|
837
|
-
|
|
838
|
-
...getShortLinkDomainsFromEnv(),
|
|
839
|
-
// Domains saved by the former Facebook "Short Link Domains" setting keep working
|
|
840
|
-
// until every environment sets CONNECTOR_SHORT_LINK_DOMAINS.
|
|
841
|
-
...parseShortLinkDomains((_e = this.context.getParameter("ShortLinkDomains")) == null ? void 0 : _e.value)
|
|
842
|
-
],
|
|
891
|
+
allowedHosts: getShortLinkDomainsFromEnv(),
|
|
843
892
|
resolvedLinksCache: cache.resolved,
|
|
844
893
|
failedLinks: cache.failed
|
|
845
894
|
});
|
|
@@ -918,7 +967,7 @@ var require_index = __commonJS({
|
|
|
918
967
|
onBatch: (batch) => this._writeBatch(writer, batch, fields)
|
|
919
968
|
});
|
|
920
969
|
await this._writeBatch(writer, data, fields);
|
|
921
|
-
});
|
|
970
|
+
}, nodeName);
|
|
922
971
|
}
|
|
923
972
|
}
|
|
924
973
|
/**
|
|
@@ -943,6 +992,16 @@ var require_index = __commonJS({
|
|
|
943
992
|
*/
|
|
944
993
|
async _processTimeSeriesNodes(nodes, accounts, state) {
|
|
945
994
|
if (!nodes.length) return;
|
|
995
|
+
const lagging = nodes.filter(
|
|
996
|
+
(node) => {
|
|
997
|
+
var _a, _b;
|
|
998
|
+
return this.source.getDateStrategy(node.name) !== DATE_STRATEGY.NONE && (((_b = (_a = this.source).getEndLagDays) == null ? void 0 : _b.call(_a, node.name)) ?? 0) > 0;
|
|
999
|
+
}
|
|
1000
|
+
);
|
|
1001
|
+
const dateRange = lagging.length ? this.getDateRange() : null;
|
|
1002
|
+
if (dateRange) {
|
|
1003
|
+
state.cursorCap = lagging.map((node) => this._nodeWindowEnd(node, dateRange)).reduce((earliest, date) => date < earliest ? date : earliest);
|
|
1004
|
+
}
|
|
946
1005
|
const dayByDayNodes = [];
|
|
947
1006
|
const windowEndDates = [];
|
|
948
1007
|
for (const node of nodes) {
|
|
@@ -981,6 +1040,12 @@ var require_index = __commonJS({
|
|
|
981
1040
|
this.context.log(LOG_LEVEL.WARN, `Could not determine date range for node "${node.name}"`);
|
|
982
1041
|
return null;
|
|
983
1042
|
}
|
|
1043
|
+
const endDate = this._nodeWindowEnd(node, dateRange);
|
|
1044
|
+
if (endDate < dateRange.startDate) {
|
|
1045
|
+
this._logLaggedOut(node, dateRange, endDate);
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
this._logLagCut(node, dateRange, endDate);
|
|
984
1049
|
const writers = /* @__PURE__ */ new Map();
|
|
985
1050
|
let completedBy = 0;
|
|
986
1051
|
this._beginPass(state);
|
|
@@ -992,17 +1057,17 @@ var require_index = __commonJS({
|
|
|
992
1057
|
fields: node.fields,
|
|
993
1058
|
accountId: (account == null ? void 0 : account.id) ?? null,
|
|
994
1059
|
startDate: dateRange.startDate,
|
|
995
|
-
endDate
|
|
1060
|
+
endDate
|
|
996
1061
|
});
|
|
997
1062
|
await this._writeBatch(writer, data, node.fields);
|
|
998
|
-
});
|
|
1063
|
+
}, node.name);
|
|
999
1064
|
if (done) completedBy += 1;
|
|
1000
1065
|
}
|
|
1001
1066
|
if (completedBy === 0 || state.passHardFailures > 0) {
|
|
1002
1067
|
state.cursorHalted = true;
|
|
1003
1068
|
return null;
|
|
1004
1069
|
}
|
|
1005
|
-
return
|
|
1070
|
+
return endDate;
|
|
1006
1071
|
}
|
|
1007
1072
|
/**
|
|
1008
1073
|
* DATE_STRATEGY.NONE: the node carries no date window at all, so there is
|
|
@@ -1042,8 +1107,15 @@ var require_index = __commonJS({
|
|
|
1042
1107
|
return;
|
|
1043
1108
|
}
|
|
1044
1109
|
const writers = /* @__PURE__ */ new Map();
|
|
1045
|
-
|
|
1110
|
+
const endDates = new Map(nodes.map((node) => [node, this._nodeWindowEnd(node, dateRange)]));
|
|
1111
|
+
for (const [node, end] of endDates) {
|
|
1112
|
+
if (end < dateRange.startDate) this._logLaggedOut(node, dateRange, end);
|
|
1113
|
+
else this._logLagCut(node, dateRange, end);
|
|
1114
|
+
}
|
|
1115
|
+
const lastDay = [...endDates.values()].reduce((latest, date) => date > latest ? date : latest);
|
|
1116
|
+
for (const date of this._iterateDates(dateRange.startDate, lastDay)) {
|
|
1046
1117
|
const formattedDate = this._formatDate(date);
|
|
1118
|
+
const dayNodes = nodes.filter((node) => endDates.get(node) >= formattedDate);
|
|
1047
1119
|
let completedBy = 0;
|
|
1048
1120
|
this._beginPass(state);
|
|
1049
1121
|
for (const account of accounts) {
|
|
@@ -1052,7 +1124,7 @@ var require_index = __commonJS({
|
|
|
1052
1124
|
continue;
|
|
1053
1125
|
}
|
|
1054
1126
|
const done = await this._runForAccount(state, account, async () => {
|
|
1055
|
-
for (const node of
|
|
1127
|
+
for (const node of dayNodes) {
|
|
1056
1128
|
const writer = this._nodeWriter(writers, node);
|
|
1057
1129
|
const data = await this.source.fetchData({
|
|
1058
1130
|
nodeName: node.name,
|
|
@@ -1063,7 +1135,7 @@ var require_index = __commonJS({
|
|
|
1063
1135
|
});
|
|
1064
1136
|
await this._writeBatch(writer, data, node.fields);
|
|
1065
1137
|
}
|
|
1066
|
-
});
|
|
1138
|
+
}, formattedDate);
|
|
1067
1139
|
if (done) completedBy += 1;
|
|
1068
1140
|
}
|
|
1069
1141
|
if (completedBy === 0 && state.passHardFailures === 0 && accounts.length > 0) {
|
|
@@ -1123,7 +1195,7 @@ var require_index = __commonJS({
|
|
|
1123
1195
|
endDate: null
|
|
1124
1196
|
});
|
|
1125
1197
|
if (data && data.length) batches.push(data);
|
|
1126
|
-
});
|
|
1198
|
+
}, nodeName);
|
|
1127
1199
|
if (!done) missing.add(this._accountKey(account));
|
|
1128
1200
|
}
|
|
1129
1201
|
if (missing.size) {
|
|
@@ -1171,6 +1243,29 @@ var require_index = __commonJS({
|
|
|
1171
1243
|
);
|
|
1172
1244
|
}
|
|
1173
1245
|
await storage.replaceData(snapshot);
|
|
1246
|
+
this._countRows(nodeName, snapshot.length);
|
|
1247
|
+
}
|
|
1248
|
+
/** @private */
|
|
1249
|
+
_countRows(nodeName, count) {
|
|
1250
|
+
if (!this._rowsByNode) return;
|
|
1251
|
+
this._rowsByNode.set(nodeName, (this._rowsByNode.get(nodeName) ?? 0) + count);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Reports 0 rows written for every node that loaded nothing in a completed run. The
|
|
1255
|
+
* storages report only the rows they write, so without this Run History showed such a run
|
|
1256
|
+
* as a plain success with no count at all.
|
|
1257
|
+
*
|
|
1258
|
+
* @param {object[]} nodes the run's planned nodes
|
|
1259
|
+
* @private
|
|
1260
|
+
*/
|
|
1261
|
+
_reportEmptyNodes(nodes) {
|
|
1262
|
+
var _a;
|
|
1263
|
+
for (const node of nodes) {
|
|
1264
|
+
if (((_a = this._rowsByNode) == null ? void 0 : _a.get(node.name)) > 0) continue;
|
|
1265
|
+
this.context.emitAnalytics("rows_written", 0, {
|
|
1266
|
+
node: this.source.getDestinationName(node.name, node.schema)
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1174
1269
|
}
|
|
1175
1270
|
/**
|
|
1176
1271
|
* One row per unique key, the last one read: what a MERGE keeps. The storages dedupe
|
|
@@ -1776,6 +1871,13 @@ var require_index = __commonJS({
|
|
|
1776
1871
|
getDateStrategy(nodeName) {
|
|
1777
1872
|
return DATE_STRATEGY.DAY_BY_DAY;
|
|
1778
1873
|
}
|
|
1874
|
+
/**
|
|
1875
|
+
* How many days before today a time-series node's window ends, for an API that reports
|
|
1876
|
+
* only completed days. 0: the window ends today.
|
|
1877
|
+
*/
|
|
1878
|
+
getEndLagDays(nodeName) {
|
|
1879
|
+
return 0;
|
|
1880
|
+
}
|
|
1779
1881
|
/**
|
|
1780
1882
|
* Called after all nodes for an account are processed.
|
|
1781
1883
|
*/
|
|
@@ -1908,14 +2010,19 @@ var require_index = __commonJS({
|
|
|
1908
2010
|
if (!response.ok) {
|
|
1909
2011
|
const bodyText = typeof response.text === "function" ? await response.text().catch(() => "") : "";
|
|
1910
2012
|
const snippet = bodyText ? ` — ${bodyText.slice(0, 300)}` : "";
|
|
1911
|
-
|
|
1912
|
-
error.response = response;
|
|
1913
|
-
error.statusCode = response.status;
|
|
1914
|
-
error.responseBody = bodyText;
|
|
2013
|
+
let payload;
|
|
1915
2014
|
try {
|
|
1916
|
-
|
|
2015
|
+
payload = JSON.parse(bodyText);
|
|
1917
2016
|
} catch {
|
|
1918
2017
|
}
|
|
2018
|
+
const providerMessage = _AbstractSource._providerErrorMessage(payload);
|
|
2019
|
+
const error = new Error(
|
|
2020
|
+
providerMessage ? `HTTP ${response.status}: ${providerMessage}` : `HTTP ${response.status}: ${response.statusText}${snippet}`
|
|
2021
|
+
);
|
|
2022
|
+
error.response = response;
|
|
2023
|
+
error.statusCode = response.status;
|
|
2024
|
+
error.responseBody = bodyText;
|
|
2025
|
+
if (payload !== void 0) error.payload = payload;
|
|
1919
2026
|
if (await this.isValidToRetry(error) && attempt < totalAttempts - 1) {
|
|
1920
2027
|
const delay = this.calculateBackoff(attempt, initialDelay);
|
|
1921
2028
|
this.context.log(
|
|
@@ -1954,6 +2061,28 @@ var require_index = __commonJS({
|
|
|
1954
2061
|
}
|
|
1955
2062
|
}
|
|
1956
2063
|
}
|
|
2064
|
+
/**
|
|
2065
|
+
* The reason a provider gave in its JSON error body, looked up where main's
|
|
2066
|
+
* _extractErrorInfo looked for it, and in a JSON:API error's `detail`. Not its `title`: that
|
|
2067
|
+
* is the same summary for every occurrence, and the body snippet it would replace names the
|
|
2068
|
+
* field in `source.pointer`.
|
|
2069
|
+
*
|
|
2070
|
+
* @param {*} payload - the parsed error body
|
|
2071
|
+
* @returns {string|null} null when the body names no message
|
|
2072
|
+
*/
|
|
2073
|
+
static _providerErrorMessage(payload) {
|
|
2074
|
+
var _a;
|
|
2075
|
+
const firstError = Array.isArray(payload == null ? void 0 : payload.errors) ? payload.errors[0] : void 0;
|
|
2076
|
+
const candidates = [
|
|
2077
|
+
(_a = payload == null ? void 0 : payload.error) == null ? void 0 : _a.message,
|
|
2078
|
+
payload == null ? void 0 : payload.message,
|
|
2079
|
+
payload == null ? void 0 : payload.errorMessage,
|
|
2080
|
+
payload == null ? void 0 : payload.error_message,
|
|
2081
|
+
firstError == null ? void 0 : firstError.message,
|
|
2082
|
+
firstError == null ? void 0 : firstError.detail
|
|
2083
|
+
];
|
|
2084
|
+
return candidates.find((message) => typeof message === "string" && message.trim()) ?? null;
|
|
2085
|
+
}
|
|
1957
2086
|
/**
|
|
1958
2087
|
* The fetch options to use for the next redirect hop.
|
|
1959
2088
|
*
|
|
@@ -2321,6 +2450,19 @@ var require_index = __commonJS({
|
|
|
2321
2450
|
return fieldsParam.value.split(",").map((field) => field.trim()).filter((field) => field !== "").map((field) => field.split(" ")).filter((field) => field.length === 2).map((field) => field[1]);
|
|
2322
2451
|
}
|
|
2323
2452
|
//----------------------------------------------------------------
|
|
2453
|
+
//---- missingSelectedFieldError ----------------------------------
|
|
2454
|
+
/**
|
|
2455
|
+
* The error for a field the Data Mart selects that the connector's schema does not have.
|
|
2456
|
+
* Its usual cause is a connector whose fields changed after the Data Mart was set up.
|
|
2457
|
+
* @param {string} columnName
|
|
2458
|
+
* @returns {Error}
|
|
2459
|
+
*/
|
|
2460
|
+
missingSelectedFieldError(columnName) {
|
|
2461
|
+
return new Error(
|
|
2462
|
+
`Field "${columnName}" is selected for import, but the connector does not provide it. If the connector's fields changed after this Data Mart was set up, open Edit Fields in the Data Mart's Input Source and save: a field the connector no longer provides is dropped.`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
//----------------------------------------------------------------
|
|
2324
2466
|
//---- getColumnType -----------------------------------------------
|
|
2325
2467
|
/**
|
|
2326
2468
|
* Get column type for storage from schema
|
|
@@ -2817,6 +2959,7 @@ var require_index = __commonJS({
|
|
|
2817
2959
|
this._activeErrorHandler = null;
|
|
2818
2960
|
this._pendingBackoff = null;
|
|
2819
2961
|
this._tokenCache = /* @__PURE__ */ new Map();
|
|
2962
|
+
this._extractedByNode = /* @__PURE__ */ new Map();
|
|
2820
2963
|
}
|
|
2821
2964
|
_compileNodes(nodes = {}) {
|
|
2822
2965
|
const schema = {};
|
|
@@ -2867,8 +3010,13 @@ var require_index = __commonJS({
|
|
|
2867
3010
|
getDateStrategy(nodeName) {
|
|
2868
3011
|
return nodeDateStrategy(this.model.nodes[nodeName]);
|
|
2869
3012
|
}
|
|
3013
|
+
getEndLagDays(nodeName) {
|
|
3014
|
+
const node = this.model.nodes[nodeName];
|
|
3015
|
+
if (nodeDateStrategy(node) === DATE_STRATEGY.NONE) return 0;
|
|
3016
|
+
return node.incremental.endLagDays ?? 0;
|
|
3017
|
+
}
|
|
2870
3018
|
async fetchData({ nodeName, fields, accountId, startDate, endDate }) {
|
|
2871
|
-
var _a, _b, _c, _d;
|
|
3019
|
+
var _a, _b, _c, _d, _e;
|
|
2872
3020
|
const node = this.model.nodes[nodeName];
|
|
2873
3021
|
if (!node) throw new Error(`DeclarativeSource: unknown node "${nodeName}"`);
|
|
2874
3022
|
const scope = this._scope({ accountId, startDate, endDate, fields });
|
|
@@ -2936,8 +3084,12 @@ var require_index = __commonJS({
|
|
|
2936
3084
|
this.runLimits
|
|
2937
3085
|
);
|
|
2938
3086
|
const rawRecords = await retriever.run(scope);
|
|
3087
|
+
const nodeTag = ((_d = this.context.getParameter("DestinationTableName")) == null ? void 0 : _d.value) ?? nodeName;
|
|
3088
|
+
this._extractedByNode.set(nodeName, {
|
|
3089
|
+
tag: nodeTag,
|
|
3090
|
+
count: (((_e = this._extractedByNode.get(nodeName)) == null ? void 0 : _e.count) ?? 0) + rawRecords.length
|
|
3091
|
+
});
|
|
2939
3092
|
if (rawRecords.length > 0) {
|
|
2940
|
-
const nodeTag = ((_d = this.context.getParameter("DestinationTableName")) == null ? void 0 : _d.value) ?? nodeName;
|
|
2941
3093
|
this.context.emitAnalytics("rows_extracted", rawRecords.length, { node: nodeTag });
|
|
2942
3094
|
}
|
|
2943
3095
|
const recordFilter = node.recordFilter ? new RecordFilter(node.recordFilter) : null;
|
|
@@ -2952,6 +3104,37 @@ var require_index = __commonJS({
|
|
|
2952
3104
|
}
|
|
2953
3105
|
return this._selectFields(new FieldCaster(node.fields || {}).cast(records), node, fields);
|
|
2954
3106
|
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Names each node whose requests got no records in the whole run. A record path that
|
|
3109
|
+
* misses the data or a wrong parameter value completes like a run with nothing new, so this
|
|
3110
|
+
* is the only hint the run leaves. INFO, not WARN: an API with nothing for the window is
|
|
3111
|
+
* also a normal outcome.
|
|
3112
|
+
*/
|
|
3113
|
+
onImportComplete() {
|
|
3114
|
+
var _a;
|
|
3115
|
+
for (const [nodeName, { tag, count }] of this._extractedByNode) {
|
|
3116
|
+
if (count > 0) continue;
|
|
3117
|
+
this.context.emitAnalytics("rows_extracted", 0, { node: tag });
|
|
3118
|
+
this.context.log(
|
|
3119
|
+
LOG_LEVEL.INFO,
|
|
3120
|
+
`No records came back for node "${nodeName}" in this run. If you expected data, check the node's record path, and the parameters and dates its request uses.` + DeclarativeSource._partitionHint((_a = this.model.nodes[nodeName]) == null ? void 0 : _a.partitionRouter)
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* A partitioned node sends one request per slice, and none when there are no slices, so
|
|
3126
|
+
* its own request is not where an empty run starts. Named as the builder's Partition form
|
|
3127
|
+
* names the fields.
|
|
3128
|
+
*/
|
|
3129
|
+
static _partitionHint(partitionRouter) {
|
|
3130
|
+
if ((partitionRouter == null ? void 0 : partitionRouter.type) === "substream") {
|
|
3131
|
+
return " Its requests run once per parent record, so also check Parent record path and Parent key: when they find nothing, the node sends no requests.";
|
|
3132
|
+
}
|
|
3133
|
+
if ((partitionRouter == null ? void 0 : partitionRouter.type) === "list") {
|
|
3134
|
+
return " Its requests run once per value of its list, so also check Values or Values from parameter: an empty list sends no requests.";
|
|
3135
|
+
}
|
|
3136
|
+
return "";
|
|
3137
|
+
}
|
|
2955
3138
|
/**
|
|
2956
3139
|
* Keeps the fields the Data Mart selected, and the node's unique keys, which the storage
|
|
2957
3140
|
* merges on. A bundled source reads only the selected fields; casting every declared one
|
|
@@ -2982,9 +3165,10 @@ var require_index = __commonJS({
|
|
|
2982
3165
|
async isValidToRetry(error) {
|
|
2983
3166
|
const handler = this._activeErrorHandler;
|
|
2984
3167
|
if (!handler) {
|
|
2985
|
-
this._pendingBackoff = null;
|
|
2986
3168
|
error._declAction = null;
|
|
2987
|
-
|
|
3169
|
+
const retry2 = this._defaultRetryable(error);
|
|
3170
|
+
this._pendingBackoff = retry2 ? { filter: null, response: error.response } : null;
|
|
3171
|
+
return retry2;
|
|
2988
3172
|
}
|
|
2989
3173
|
let bodyText = "";
|
|
2990
3174
|
let bodyJson = null;
|
|
@@ -3003,13 +3187,23 @@ var require_index = __commonJS({
|
|
|
3003
3187
|
return retry;
|
|
3004
3188
|
}
|
|
3005
3189
|
calculateBackoff(attempt, initialDelay) {
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
const ms = this._activeErrorHandler.delayMs(
|
|
3190
|
+
const pending = this._pendingBackoff;
|
|
3191
|
+
this._pendingBackoff = null;
|
|
3192
|
+
if (pending && this._activeErrorHandler) {
|
|
3193
|
+
const ms = this._activeErrorHandler.delayMs(
|
|
3194
|
+
pending.filter,
|
|
3195
|
+
pending.response,
|
|
3196
|
+
attempt,
|
|
3197
|
+
initialDelay
|
|
3198
|
+
);
|
|
3010
3199
|
if (ms != null) return ms;
|
|
3011
3200
|
}
|
|
3012
|
-
|
|
3201
|
+
const fallback = super.calculateBackoff(attempt, initialDelay);
|
|
3202
|
+
if (pending && !process.env.OW_TEST) {
|
|
3203
|
+
const ms = retryAfterDelayMs(pending.response);
|
|
3204
|
+
if (ms != null) return Math.max(ms, fallback);
|
|
3205
|
+
}
|
|
3206
|
+
return fallback;
|
|
3013
3207
|
}
|
|
3014
3208
|
/**
|
|
3015
3209
|
* A 401 or 403 skips the account as refused access, but not one a filter mapped to FAIL:
|
|
@@ -3220,6 +3414,16 @@ var require_index = __commonJS({
|
|
|
3220
3414
|
if (!Number.isFinite(ms)) return null;
|
|
3221
3415
|
return Math.min(Math.max(0, ms), MAX_HEADER_RETRY_DELAY_MS);
|
|
3222
3416
|
}
|
|
3417
|
+
function retryAfterDelayMs(response, headerName = "Retry-After") {
|
|
3418
|
+
var _a, _b;
|
|
3419
|
+
const raw = (_b = (_a = response == null ? void 0 : response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, headerName);
|
|
3420
|
+
if (raw == null) return null;
|
|
3421
|
+
const secs = Number(raw);
|
|
3422
|
+
if (Number.isFinite(secs)) return clampHeaderDelay(secs * 1e3);
|
|
3423
|
+
const dateMs = Date.parse(raw);
|
|
3424
|
+
if (!Number.isNaN(dateMs)) return clampHeaderDelay(dateMs - Date.now());
|
|
3425
|
+
return null;
|
|
3426
|
+
}
|
|
3223
3427
|
class ErrorHandler {
|
|
3224
3428
|
/**
|
|
3225
3429
|
* @param {{ responseFilters?: Array<object>, backoff?: object }} config
|
|
@@ -3290,16 +3494,7 @@ var require_index = __commonJS({
|
|
|
3290
3494
|
}
|
|
3291
3495
|
}
|
|
3292
3496
|
_waitTimeFromHeader(spec, response) {
|
|
3293
|
-
|
|
3294
|
-
if (!response) return null;
|
|
3295
|
-
const headerName = spec && spec.header || "Retry-After";
|
|
3296
|
-
const raw = (_b = (_a = response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, headerName);
|
|
3297
|
-
if (raw == null) return null;
|
|
3298
|
-
const secs = Number(raw);
|
|
3299
|
-
if (Number.isFinite(secs)) return clampHeaderDelay(secs * 1e3);
|
|
3300
|
-
const dateMs = Date.parse(raw);
|
|
3301
|
-
if (!Number.isNaN(dateMs)) return clampHeaderDelay(dateMs - Date.now());
|
|
3302
|
-
return null;
|
|
3497
|
+
return retryAfterDelayMs(response, spec && spec.header || "Retry-After");
|
|
3303
3498
|
}
|
|
3304
3499
|
_waitUntilTimeFromHeader(spec, response) {
|
|
3305
3500
|
var _a, _b;
|
|
@@ -3497,6 +3692,45 @@ var require_index = __commonJS({
|
|
|
3497
3692
|
const PAGINATION_TYPES = /* @__PURE__ */ new Set(["none", "offset", "page", "cursor"]);
|
|
3498
3693
|
const INJECT_TARGETS = /* @__PURE__ */ new Set(["query", "header", "body", "path"]);
|
|
3499
3694
|
const RESPONSE_FORMATS = /* @__PURE__ */ new Set(["json", "csv", "jsonl"]);
|
|
3695
|
+
const MAX_END_LAG_DAYS = 30;
|
|
3696
|
+
const DATE_WINDOW_PLACEHOLDER = /\{\{\s*(dateWindow(?:\.[a-zA-Z0-9_]+)*)\s*\}\}/;
|
|
3697
|
+
function dateWindowTemplateIn(value) {
|
|
3698
|
+
var _a;
|
|
3699
|
+
if (typeof value === "string") return ((_a = DATE_WINDOW_PLACEHOLDER.exec(value)) == null ? void 0 : _a[1]) ?? null;
|
|
3700
|
+
if (value && typeof value === "object") {
|
|
3701
|
+
for (const item of Object.values(value)) {
|
|
3702
|
+
const path = dateWindowTemplateIn(item);
|
|
3703
|
+
if (path) return path;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
return null;
|
|
3707
|
+
}
|
|
3708
|
+
function assertNoStrictDateWindowTemplate(nodeName, node) {
|
|
3709
|
+
var _a, _b, _c;
|
|
3710
|
+
const isAsync = ((_a = node.retriever) == null ? void 0 : _a.type) === "async";
|
|
3711
|
+
for (const [where, spec] of [
|
|
3712
|
+
// An async node sends its retriever's requests, never `request`.
|
|
3713
|
+
["request", isAsync ? void 0 : node.request],
|
|
3714
|
+
["retriever.submit", isAsync ? node.retriever.submit : void 0],
|
|
3715
|
+
["retriever.poll", isAsync ? node.retriever.poll : void 0],
|
|
3716
|
+
["partitionRouter.parent.request", (_c = (_b = node.partitionRouter) == null ? void 0 : _b.parent) == null ? void 0 : _c.request]
|
|
3717
|
+
]) {
|
|
3718
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3719
|
+
const sendsBody = String(spec.method || "GET").toUpperCase() !== "GET";
|
|
3720
|
+
for (const [place, value] of [
|
|
3721
|
+
[`${where}.path`, spec.path],
|
|
3722
|
+
[`${where}.body`, sendsBody ? spec.body : void 0]
|
|
3723
|
+
]) {
|
|
3724
|
+
const path = dateWindowTemplateIn(value);
|
|
3725
|
+
if (path) {
|
|
3726
|
+
const fix = node.isFullRefresh ? 'Remove "isFullRefresh" and add an "incremental" block to fetch by date window, or remove the template.' : 'Add an "incremental" block to the node, or remove the template.';
|
|
3727
|
+
throw new Error(
|
|
3728
|
+
`ManifestParser: node "${nodeName}" uses "{{ ${path} }}" in ${place}, but the node is never given a date window: only a node with an "incremental" strategy, and without "isFullRefresh", is fetched one window at a time. ${fix}`
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3500
3734
|
const LEGACY_INJECT_PARAM = {
|
|
3501
3735
|
cursor: "cursorParam",
|
|
3502
3736
|
offset: "offsetParam",
|
|
@@ -3620,10 +3854,12 @@ var require_index = __commonJS({
|
|
|
3620
3854
|
class ManifestParser {
|
|
3621
3855
|
/**
|
|
3622
3856
|
* @param {string} json
|
|
3857
|
+
* @param {{ authoring?: boolean }} [options] - `authoring` adds the checks for a manifest
|
|
3858
|
+
* being published or tested, which a stored version must not start failing on.
|
|
3623
3859
|
* @returns {object} ManifestModel
|
|
3624
3860
|
*/
|
|
3625
|
-
parse(json) {
|
|
3626
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
3861
|
+
parse(json, { authoring = false } = {}) {
|
|
3862
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A;
|
|
3627
3863
|
let raw;
|
|
3628
3864
|
try {
|
|
3629
3865
|
raw = JSON.parse(json);
|
|
@@ -3799,11 +4035,27 @@ var require_index = __commonJS({
|
|
|
3799
4035
|
`ManifestParser: node "${nodeName}" has unknown incremental.strategy "${strategy}"`
|
|
3800
4036
|
);
|
|
3801
4037
|
}
|
|
4038
|
+
const endLagDays = (_w = node.incremental) == null ? void 0 : _w.endLagDays;
|
|
4039
|
+
if (endLagDays !== void 0) {
|
|
4040
|
+
if (!Number.isInteger(endLagDays) || endLagDays < 0 || endLagDays > MAX_END_LAG_DAYS) {
|
|
4041
|
+
throw new Error(
|
|
4042
|
+
`ManifestParser: node "${nodeName}" incremental.endLagDays must be a whole number of days from 0 to ${MAX_END_LAG_DAYS}, e.g. 1 to leave out today`
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
if (strategy === "none") {
|
|
4046
|
+
throw new Error(
|
|
4047
|
+
`ManifestParser: node "${nodeName}" incremental.endLagDays needs a "day-by-day" or "range" strategy — a node with no date window has no window end to move`
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
3802
4051
|
if (declaresDateWindow(node) && node.isFullRefresh) {
|
|
3803
4052
|
throw new Error(
|
|
3804
4053
|
`ManifestParser: node "${nodeName}" declares both "incremental" and "isFullRefresh", which are mutually exclusive — a full-refresh node replaces the whole table and is never given a date window, so the incremental block would be silently ignored. Remove "isFullRefresh" to fetch by date window, or remove the "incremental" block.`
|
|
3805
4054
|
);
|
|
3806
4055
|
}
|
|
4056
|
+
if (authoring && !declaresDateWindow(node)) {
|
|
4057
|
+
assertNoStrictDateWindowTemplate(nodeName, node);
|
|
4058
|
+
}
|
|
3807
4059
|
if (node.transformations !== void 0) {
|
|
3808
4060
|
if (!Array.isArray(node.transformations)) {
|
|
3809
4061
|
throw new Error(`ManifestParser: node "${nodeName}" transformations must be an array`);
|
|
@@ -3899,7 +4151,7 @@ var require_index = __commonJS({
|
|
|
3899
4151
|
}
|
|
3900
4152
|
}
|
|
3901
4153
|
if (eh.backoff !== void 0) validateBackoff(eh.backoff, nodeName, "errorHandler");
|
|
3902
|
-
if (((
|
|
4154
|
+
if (((_x = node.retriever) == null ? void 0 : _x.type) === "async") {
|
|
3903
4155
|
throw new Error(
|
|
3904
4156
|
`ManifestParser: node "${nodeName}" errorHandler is not supported with an async retriever — an async node paces itself through "retriever.poll.backoff" instead, so these filters and this backoff would never run. Remove "errorHandler", or make the node sync.`
|
|
3905
4157
|
);
|
|
@@ -3952,7 +4204,7 @@ var require_index = __commonJS({
|
|
|
3952
4204
|
);
|
|
3953
4205
|
}
|
|
3954
4206
|
}
|
|
3955
|
-
if (((
|
|
4207
|
+
if (((_y = node.retriever) == null ? void 0 : _y.type) === "async") {
|
|
3956
4208
|
throw new Error(
|
|
3957
4209
|
`ManifestParser: node "${nodeName}" partitionRouter is not supported with an async retriever`
|
|
3958
4210
|
);
|
|
@@ -3998,7 +4250,7 @@ var require_index = __commonJS({
|
|
|
3998
4250
|
}
|
|
3999
4251
|
}
|
|
4000
4252
|
}
|
|
4001
|
-
const rType = ((
|
|
4253
|
+
const rType = ((_z = node.retriever) == null ? void 0 : _z.type) ?? "sync";
|
|
4002
4254
|
if (rType === "sync") {
|
|
4003
4255
|
if (!node.request) {
|
|
4004
4256
|
throw new Error(`ManifestParser: node "${nodeName}" is missing "request"`);
|
|
@@ -4130,7 +4382,7 @@ var require_index = __commonJS({
|
|
|
4130
4382
|
);
|
|
4131
4383
|
}
|
|
4132
4384
|
for (const key of ["maxAttempts", "initialMs", "maxMs"]) {
|
|
4133
|
-
const value = (
|
|
4385
|
+
const value = (_A = r.poll.backoff) == null ? void 0 : _A[key];
|
|
4134
4386
|
if (value === void 0) continue;
|
|
4135
4387
|
const valid = typeof value === "number" && value > 0 && (key === "maxAttempts" ? Number.isInteger(value) : Number.isFinite(value));
|
|
4136
4388
|
if (!valid) {
|
|
@@ -4209,7 +4461,7 @@ var require_index = __commonJS({
|
|
|
4209
4461
|
const identifier = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4210
4462
|
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(nodeName)) {
|
|
4211
4463
|
throw new Error(
|
|
4212
|
-
`ManifestParser: node "${nodeName}" must start with a letter and contain only letters, digits and underscores`
|
|
4464
|
+
`ManifestParser: node "${nodeName}" must start with a letter and contain only letters, digits and underscores` + (nodeName.includes("/") ? `; the API path goes in the node's request.path` : "")
|
|
4213
4465
|
);
|
|
4214
4466
|
}
|
|
4215
4467
|
if (node.destinationName !== void 0 && !identifier.test(String(node.destinationName))) {
|
|
@@ -5029,6 +5281,9 @@ var require_index = __commonJS({
|
|
|
5029
5281
|
];
|
|
5030
5282
|
const CONCLUSIVE_DNS_FAILURES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA"]);
|
|
5031
5283
|
const INCONCLUSIVE_LOOKUP_ATTEMPTS = 3;
|
|
5284
|
+
function refusal(message) {
|
|
5285
|
+
return Object.assign(new Error(message), { isRefusal: true });
|
|
5286
|
+
}
|
|
5032
5287
|
class SsrfGuard {
|
|
5033
5288
|
/**
|
|
5034
5289
|
* True if `ip` is a private/loopback/link-local address. Accepts both the
|
|
@@ -5138,11 +5393,11 @@ var require_index = __commonJS({
|
|
|
5138
5393
|
}
|
|
5139
5394
|
if (SsrfGuard._allowLocalEgress()) return;
|
|
5140
5395
|
if (url.protocol !== "https:") {
|
|
5141
|
-
throw
|
|
5396
|
+
throw refusal(`SsrfGuard: URL must use https, got "${url.protocol}"`);
|
|
5142
5397
|
}
|
|
5143
5398
|
const host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
5144
5399
|
if (this._isBlockedLiteralHost(host)) {
|
|
5145
|
-
throw
|
|
5400
|
+
throw refusal(`SsrfGuard: blocked IP/host "${host}"`);
|
|
5146
5401
|
}
|
|
5147
5402
|
await this._assertResolvesToPublic(host);
|
|
5148
5403
|
}
|
|
@@ -5192,7 +5447,7 @@ var require_index = __commonJS({
|
|
|
5192
5447
|
for (const a of list) {
|
|
5193
5448
|
const address = a && typeof a === "object" ? a.address : a;
|
|
5194
5449
|
if (SsrfGuard.isBlockedIp(address)) {
|
|
5195
|
-
throw
|
|
5450
|
+
throw refusal(`SsrfGuard: host "${host}" resolves to blocked address "${address}"`);
|
|
5196
5451
|
}
|
|
5197
5452
|
}
|
|
5198
5453
|
}
|
|
@@ -6626,25 +6881,28 @@ var require_index = __commonJS({
|
|
|
6626
6881
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
6627
6882
|
}
|
|
6628
6883
|
};
|
|
6629
|
-
async function resolveShortLinkFields(data, specs, {
|
|
6884
|
+
async function resolveShortLinkFields(data, specs, { allowedHosts = [], resolvedLinksCache = /* @__PURE__ */ new Map(), failedLinks = /* @__PURE__ */ new Set() } = {}) {
|
|
6630
6885
|
if (!Array.isArray(data) || data.length === 0 || !Array.isArray(specs) || specs.length === 0) return data;
|
|
6631
|
-
const candidates = _collectCandidateUrls(data, specs,
|
|
6886
|
+
const candidates = _collectCandidateUrls(data, specs, allowedHosts);
|
|
6632
6887
|
const hasPlainTargets = specs.some((spec) => !spec.urlKey);
|
|
6633
6888
|
if (candidates.length === 0 && !hasPlainTargets) return data;
|
|
6634
6889
|
const uncachedLinks = candidates.filter((url) => !resolvedLinksCache.has(url)).map((originalUrl) => ({ originalUrl }));
|
|
6635
|
-
const freshlyResolved = await _resolveShortLinks(uncachedLinks);
|
|
6890
|
+
const freshlyResolved = await _resolveShortLinks(uncachedLinks, allowedHosts);
|
|
6636
6891
|
freshlyResolved.forEach((link) => {
|
|
6637
6892
|
resolvedLinksCache.set(link.originalUrl, link.resolvedUrl);
|
|
6638
6893
|
if (!link.ok) failedLinks.add(link.originalUrl);
|
|
6639
6894
|
});
|
|
6640
|
-
|
|
6895
|
+
const resolvable = new Set(candidates);
|
|
6896
|
+
return data.map(
|
|
6897
|
+
(record) => specs.reduce((current, spec) => _applySpec(current, spec, resolvedLinksCache, resolvable), record)
|
|
6898
|
+
);
|
|
6641
6899
|
}
|
|
6642
|
-
function _collectCandidateUrls(data, specs,
|
|
6900
|
+
function _collectCandidateUrls(data, specs, allowedHosts) {
|
|
6643
6901
|
const unique = /* @__PURE__ */ new Set();
|
|
6644
6902
|
data.forEach((record) => {
|
|
6645
6903
|
specs.forEach((spec) => {
|
|
6646
6904
|
_readUrls(record[spec.field], spec).forEach((url) => {
|
|
6647
|
-
if (_isPotentialShortLink(url,
|
|
6905
|
+
if (_isPotentialShortLink(url, allowedHosts)) unique.add(url);
|
|
6648
6906
|
});
|
|
6649
6907
|
});
|
|
6650
6908
|
});
|
|
@@ -6669,9 +6927,9 @@ var require_index = __commonJS({
|
|
|
6669
6927
|
}
|
|
6670
6928
|
return null;
|
|
6671
6929
|
}
|
|
6672
|
-
function _applySpec(record, spec, cache) {
|
|
6930
|
+
function _applySpec(record, spec, cache, resolvable) {
|
|
6673
6931
|
const value = record[spec.field];
|
|
6674
|
-
const resolve = (url) => typeof url === "string" && cache.has(url) ? cache.get(url) : url;
|
|
6932
|
+
const resolve = (url) => typeof url === "string" && resolvable.has(url) && cache.has(url) ? cache.get(url) : url;
|
|
6675
6933
|
if (spec.urlKey) {
|
|
6676
6934
|
const original = value && value[spec.urlKey];
|
|
6677
6935
|
const resolved = resolve(original);
|
|
@@ -6696,7 +6954,15 @@ var require_index = __commonJS({
|
|
|
6696
6954
|
}
|
|
6697
6955
|
function parseShortLinkDomains(value) {
|
|
6698
6956
|
if (!value) return [];
|
|
6699
|
-
return String(value).split(/[,;\s]+/).map((entry) => entry.replace(/^[a-z]+:\/\//i, "").split("/")[0].split(":")[0].replace(/\.$/, "").trim().toLowerCase()).filter((host) => host.includes("."));
|
|
6957
|
+
return String(value).split(/[,;\s]+/).map((entry) => entry.replace(/^[a-z]+:\/\//i, "").replace(/^\*?\./, "").split("/")[0].split(":")[0].replace(/\.$/, "").trim().toLowerCase()).map(_toAsciiHost).filter((host) => host.includes(".") && !/^\d+(\.\d+){3}$/.test(host));
|
|
6958
|
+
}
|
|
6959
|
+
function _toAsciiHost(host) {
|
|
6960
|
+
if (!host) return "";
|
|
6961
|
+
try {
|
|
6962
|
+
return new URL(`https://${host}`).hostname;
|
|
6963
|
+
} catch (_error) {
|
|
6964
|
+
return "";
|
|
6965
|
+
}
|
|
6700
6966
|
}
|
|
6701
6967
|
const SHORT_LINKS_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6702
6968
|
const SHORT_LINKS_STATE_MAX_BYTES = 24 * 1024;
|
|
@@ -6725,60 +6991,94 @@ var require_index = __commonJS({
|
|
|
6725
6991
|
return state;
|
|
6726
6992
|
}
|
|
6727
6993
|
const UTM_PARAM_PATTERN = /utm_(source|medium|campaign|term|content)/;
|
|
6728
|
-
|
|
6994
|
+
const KNOWN_SHORT_LINK_DOMAINS = [
|
|
6995
|
+
"bit.ly",
|
|
6996
|
+
"tinyurl.com",
|
|
6997
|
+
"t.co",
|
|
6998
|
+
"lnkd.in",
|
|
6999
|
+
"youtu.be",
|
|
7000
|
+
"amzn.to",
|
|
7001
|
+
"ow.ly",
|
|
7002
|
+
"buff.ly",
|
|
7003
|
+
"cutt.ly",
|
|
7004
|
+
"is.gd",
|
|
7005
|
+
"rebrand.ly"
|
|
7006
|
+
];
|
|
7007
|
+
function _isPotentialShortLink(url, allowedHosts) {
|
|
6729
7008
|
if (!url || typeof url !== "string") return false;
|
|
6730
7009
|
try {
|
|
6731
7010
|
const parsedUrl = new URL(url);
|
|
6732
7011
|
if (parsedUrl.protocol !== "https:" || url.includes("?") || UTM_PARAM_PATTERN.test(url)) {
|
|
6733
7012
|
return false;
|
|
6734
7013
|
}
|
|
6735
|
-
|
|
6736
|
-
const pathSegments = _getPathSegments(parsedUrl.pathname
|
|
6737
|
-
|
|
6738
|
-
return pathSegments.length === 1 || isConfiguredHost;
|
|
7014
|
+
if (!_isAllowedShortLinkHost(parsedUrl.hostname, allowedHosts)) return false;
|
|
7015
|
+
const pathSegments = _getPathSegments(parsedUrl.pathname);
|
|
7016
|
+
return pathSegments.every(Boolean);
|
|
6739
7017
|
} catch (_error) {
|
|
6740
7018
|
return false;
|
|
6741
7019
|
}
|
|
6742
7020
|
}
|
|
6743
|
-
function _getPathSegments(pathname
|
|
7021
|
+
function _getPathSegments(pathname) {
|
|
6744
7022
|
const segments = pathname.slice(1).split("/");
|
|
6745
7023
|
const hasTrailingSlash = segments.length > 1 && segments[segments.length - 1] === "";
|
|
6746
|
-
return
|
|
7024
|
+
return hasTrailingSlash ? segments.slice(0, -1) : segments;
|
|
6747
7025
|
}
|
|
6748
|
-
function
|
|
6749
|
-
|
|
7026
|
+
function _isAllowedShortLinkHost(hostname, allowedHosts) {
|
|
7027
|
+
const isMatch = (host) => hostname === host || hostname.endsWith(`.${host}`);
|
|
7028
|
+
return KNOWN_SHORT_LINK_DOMAINS.some(isMatch) || allowedHosts.some(isMatch);
|
|
6750
7029
|
}
|
|
6751
7030
|
const SHORT_LINK_FETCH_TIMEOUT_MS = 1e4;
|
|
6752
7031
|
const SHORT_LINK_MAX_REDIRECTS = 20;
|
|
6753
7032
|
const SHORT_LINK_CONCURRENCY = 10;
|
|
6754
|
-
async function _resolveShortLinks(shortLinks) {
|
|
7033
|
+
async function _resolveShortLinks(shortLinks, allowedHosts = []) {
|
|
6755
7034
|
const results = [];
|
|
6756
7035
|
for (let i = 0; i < shortLinks.length; i += SHORT_LINK_CONCURRENCY) {
|
|
6757
7036
|
const batch = shortLinks.slice(i, i + SHORT_LINK_CONCURRENCY);
|
|
6758
|
-
results.push(...await Promise.all(batch.map(_resolveShortLink)));
|
|
7037
|
+
results.push(...await Promise.all(batch.map((link) => _resolveShortLink(link, allowedHosts))));
|
|
6759
7038
|
}
|
|
6760
7039
|
return results;
|
|
6761
7040
|
}
|
|
6762
|
-
async function _resolveShortLink(linkObj) {
|
|
7041
|
+
async function _resolveShortLink(linkObj, allowedHosts = []) {
|
|
6763
7042
|
const originalUrl = linkObj.originalUrl;
|
|
6764
7043
|
try {
|
|
6765
|
-
return { originalUrl, resolvedUrl: await _followRedirects(originalUrl), ok: true };
|
|
7044
|
+
return { originalUrl, resolvedUrl: await _followRedirects(originalUrl, allowedHosts), ok: true };
|
|
6766
7045
|
} catch (error) {
|
|
6767
|
-
console.log(`Failed to resolve short link ${originalUrl}: ${error.message}`);
|
|
6768
|
-
return { originalUrl, resolvedUrl: originalUrl, ok:
|
|
7046
|
+
console.log(`Failed to resolve short link ${_describeUrl(originalUrl)}: ${error.message}`);
|
|
7047
|
+
return { originalUrl, resolvedUrl: originalUrl, ok: Boolean(error.isRefusal) };
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
function _describeUrl(url) {
|
|
7051
|
+
try {
|
|
7052
|
+
const { origin, pathname } = new URL(url);
|
|
7053
|
+
return `${origin}${pathname}`;
|
|
7054
|
+
} catch (_error) {
|
|
7055
|
+
return "[unparseable URL]";
|
|
6769
7056
|
}
|
|
6770
7057
|
}
|
|
6771
|
-
|
|
7058
|
+
const SHORT_LINK_TRANSIENT_STATUS = (status) => status === 408 || status === 429 || status >= 500;
|
|
7059
|
+
async function _followRedirects(startUrl, allowedHosts = []) {
|
|
7060
|
+
var _a;
|
|
6772
7061
|
let currentUrl = startUrl;
|
|
6773
7062
|
for (let hop = 0; hop <= SHORT_LINK_MAX_REDIRECTS; hop++) {
|
|
6774
7063
|
if (!_isPublicHttpUrl(currentUrl)) {
|
|
6775
|
-
|
|
7064
|
+
const refusal2 = new Error(`Refusing non-public URL ${_describeUrl(currentUrl)}`);
|
|
7065
|
+
refusal2.isRefusal = true;
|
|
7066
|
+
throw refusal2;
|
|
6776
7067
|
}
|
|
7068
|
+
if (hop > 0 && !_isAllowedShortLinkHost(new URL(currentUrl).hostname, allowedHosts)) {
|
|
7069
|
+
return currentUrl;
|
|
7070
|
+
}
|
|
7071
|
+
await new SsrfGuard([]).assertPublicHttps(currentUrl);
|
|
6777
7072
|
const response = await fetch(currentUrl, {
|
|
6778
7073
|
method: "GET",
|
|
6779
7074
|
redirect: "manual",
|
|
6780
7075
|
signal: AbortSignal.timeout(SHORT_LINK_FETCH_TIMEOUT_MS)
|
|
6781
7076
|
});
|
|
7077
|
+
await ((_a = response.body) == null ? void 0 : _a.cancel().catch(() => {
|
|
7078
|
+
}));
|
|
7079
|
+
if (SHORT_LINK_TRANSIENT_STATUS(response.status)) {
|
|
7080
|
+
throw new Error(`Short link service answered ${response.status}`);
|
|
7081
|
+
}
|
|
6782
7082
|
const location = _getRedirectLocation(response);
|
|
6783
7083
|
if (!location) return currentUrl;
|
|
6784
7084
|
currentUrl = new URL(location, currentUrl).toString();
|
|
@@ -7087,7 +7387,7 @@ var require_index = __commonJS({
|
|
|
7087
7387
|
let columnName = tableColumns[i];
|
|
7088
7388
|
let columnDescription = "";
|
|
7089
7389
|
if (!(columnName in this.schema)) {
|
|
7090
|
-
throw
|
|
7390
|
+
throw this.missingSelectedFieldError(columnName);
|
|
7091
7391
|
}
|
|
7092
7392
|
let columnType = this.getColumnType(columnName);
|
|
7093
7393
|
if ("description" in this.schema[columnName]) {
|
|
@@ -7715,7 +8015,7 @@ ${columnDef}`);
|
|
|
7715
8015
|
let columnName = tableColumns[i];
|
|
7716
8016
|
let columnDescription = "";
|
|
7717
8017
|
if (!(columnName in this.schema)) {
|
|
7718
|
-
throw
|
|
8018
|
+
throw this.missingSelectedFieldError(columnName);
|
|
7719
8019
|
}
|
|
7720
8020
|
let columnType = this.getColumnType(columnName);
|
|
7721
8021
|
if ("description" in this.schema[columnName]) {
|
|
@@ -8741,7 +9041,7 @@ COMMIT TRANSACTION;`;
|
|
|
8741
9041
|
let columnName = tableColumns[i];
|
|
8742
9042
|
let columnDescription = "";
|
|
8743
9043
|
if (!(columnName in this.schema)) {
|
|
8744
|
-
throw
|
|
9044
|
+
throw this.missingSelectedFieldError(columnName);
|
|
8745
9045
|
}
|
|
8746
9046
|
let columnType = this.getColumnType(columnName);
|
|
8747
9047
|
const isPartOfPK = pkColumns.includes(columnName);
|
|
@@ -12344,7 +12644,7 @@ Query: ${query}`);
|
|
|
12344
12644
|
requiredType: "boolean",
|
|
12345
12645
|
default: true,
|
|
12346
12646
|
label: "Process Short Links",
|
|
12347
|
-
description: "Resolve short links in landing URL fields to their
|
|
12647
|
+
description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example website_url_parsed)",
|
|
12348
12648
|
attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
|
|
12349
12649
|
},
|
|
12350
12650
|
CreateEmptyTables: {
|
|
@@ -14336,7 +14636,7 @@ Query: ${query}`);
|
|
|
14336
14636
|
requiredType: "boolean",
|
|
14337
14637
|
default: true,
|
|
14338
14638
|
label: "Process Short Links",
|
|
14339
|
-
description: "Resolve short links in landing URL fields to their
|
|
14639
|
+
description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example landing_page_url_parsed)",
|
|
14340
14640
|
attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
|
|
14341
14641
|
},
|
|
14342
14642
|
CreateEmptyTables: {
|
|
@@ -19970,7 +20270,7 @@ Query: ${query}`);
|
|
|
19970
20270
|
requiredType: "boolean",
|
|
19971
20271
|
default: true,
|
|
19972
20272
|
label: "Process Short Links",
|
|
19973
|
-
description: "Resolve short links in landing URL fields to their
|
|
20273
|
+
description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example click_url_parsed)",
|
|
19974
20274
|
attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
|
|
19975
20275
|
},
|
|
19976
20276
|
CreateEmptyTables: {
|
|
@@ -22147,7 +22447,7 @@ API Response: ${JSON.stringify(statusResult, null, 2)}`);
|
|
|
22147
22447
|
requiredType: "boolean",
|
|
22148
22448
|
default: true,
|
|
22149
22449
|
label: "Process Short Links",
|
|
22150
|
-
description: "Resolve short links in landing URL fields to their
|
|
22450
|
+
description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example FinalUrlParsed)",
|
|
22151
22451
|
attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
|
|
22152
22452
|
},
|
|
22153
22453
|
CreateEmptyTables: {
|
|
@@ -26850,7 +27150,7 @@ API Response: ${JSON.stringify(statusResult, null, 2)}`);
|
|
|
26850
27150
|
requiredType: "boolean",
|
|
26851
27151
|
default: true,
|
|
26852
27152
|
label: "Process Short Links",
|
|
26853
|
-
description: "Resolve short links in landing URL fields to their
|
|
27153
|
+
description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example ad_final_urls_parsed)",
|
|
26854
27154
|
attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
|
|
26855
27155
|
},
|
|
26856
27156
|
CreateEmptyTables: {
|
|
@@ -34399,7 +34699,7 @@ API Response: ${JSON.stringify(statusResult, null, 2)}`);
|
|
|
34399
34699
|
requiredType: "boolean",
|
|
34400
34700
|
default: true,
|
|
34401
34701
|
label: "Process Short Links",
|
|
34402
|
-
description: "Resolve short links to their
|
|
34702
|
+
description: "Resolve short links to the address their short link service points to: link_url_asset.parsed_url on the Ad Insights by Link URL Asset endpoint, and link_url_parsed / object_url_parsed on Ad Creatives",
|
|
34403
34703
|
attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
|
|
34404
34704
|
},
|
|
34405
34705
|
CreateEmptyTables: {
|