Migrate remaining legacy operator workflows
This commit is contained in:
235
Web/comparison-import-workflow.js
Normal file
235
Web/comparison-import-workflow.js
Normal file
@@ -0,0 +1,235 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
|
||||
const comparison = typeof module === "object" && module.exports
|
||||
? require("./comparison-workflow.js")
|
||||
: root?.MbnComparisonWorkflow;
|
||||
const api = Object.freeze(factory(comparison));
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
if (root) root.MbnComparisonImportWorkflow = api;
|
||||
})(typeof globalThis === "object" ? globalThis : this, function (comparison) {
|
||||
"use strict";
|
||||
|
||||
if (!comparison) throw new Error("Comparison workflow dependency is unavailable.");
|
||||
|
||||
const IMPORT_SCHEMA_VERSION = 1;
|
||||
const STORAGE_ENVELOPE_SCHEMA_VERSION = 1;
|
||||
const MAXIMUM_PAIRS = 500;
|
||||
const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/;
|
||||
const digestPattern = /^[A-F0-9]{64}$/;
|
||||
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/;
|
||||
const controlCharacterPattern = /[\u0000-\u001f\u007f]/u;
|
||||
const importedRecordIdPattern = /^legacy-compare-[a-f0-9]{12}-[1-9][0-9]{0,2}$/u;
|
||||
|
||||
function hasOnlyKeys(value, expected) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const keys = Object.keys(value).sort();
|
||||
const wanted = [...expected].sort();
|
||||
return keys.length === wanted.length && keys.every((key, index) => key === wanted[index]);
|
||||
}
|
||||
|
||||
function safeText(value, maximumLength) {
|
||||
return typeof value === "string" && value.length > 0 && value.length <= maximumLength &&
|
||||
value === value.trim() && !controlCharacterPattern.test(value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizeWireTarget(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
||||
typeof value.kind !== "string") return null;
|
||||
let candidate;
|
||||
switch (value.kind) {
|
||||
case "market-target":
|
||||
if (!hasOnlyKeys(value, ["kind", "target"]) || !safeText(value.target, 64)) return null;
|
||||
candidate = { kind: value.kind, target: value.target };
|
||||
break;
|
||||
case "krx-stock":
|
||||
case "nxt-stock":
|
||||
if (!hasOnlyKeys(value, ["kind", "market", "stockName", "stockCode"]) ||
|
||||
!safeText(value.market, 8) || !safeText(value.stockName, 200) ||
|
||||
!safeText(value.stockCode, 32)) return null;
|
||||
candidate = {
|
||||
kind: value.kind,
|
||||
market: value.market,
|
||||
stockName: value.stockName,
|
||||
stockCode: value.stockCode
|
||||
};
|
||||
break;
|
||||
case "world-stock":
|
||||
if (!hasOnlyKeys(value, ["kind", "inputName", "symbol", "nation"]) ||
|
||||
!safeText(value.inputName, 200) || !safeText(value.symbol, 64) ||
|
||||
!safeText(value.nation, 2)) return null;
|
||||
candidate = {
|
||||
kind: value.kind,
|
||||
inputName: value.inputName,
|
||||
displayName: value.inputName,
|
||||
symbol: value.symbol,
|
||||
nation: value.nation
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return comparison.normalizeTarget(candidate);
|
||||
}
|
||||
|
||||
function normalizeImportResponse(value, expectedRequestId) {
|
||||
if (!requestIdPattern.test(String(expectedRequestId || "")) ||
|
||||
!hasOnlyKeys(value, ["requestId", "sourceSha256", "sourceRowCount", "retrievedAt", "pairs"]) ||
|
||||
value.requestId !== expectedRequestId ||
|
||||
typeof value.sourceSha256 !== "string" || !digestPattern.test(value.sourceSha256) ||
|
||||
!Number.isInteger(value.sourceRowCount) || value.sourceRowCount < 1 ||
|
||||
value.sourceRowCount > MAXIMUM_PAIRS ||
|
||||
typeof value.retrievedAt !== "string" || !isoDatePattern.test(value.retrievedAt) ||
|
||||
!Number.isFinite(Date.parse(value.retrievedAt)) ||
|
||||
!Array.isArray(value.pairs) || value.pairs.length !== value.sourceRowCount) return null;
|
||||
|
||||
const pairs = [];
|
||||
const signatures = new Set();
|
||||
for (let index = 0; index < value.pairs.length; index += 1) {
|
||||
const raw = value.pairs[index];
|
||||
if (!hasOnlyKeys(raw, ["rowNumber", "first", "second"]) ||
|
||||
raw.rowNumber !== index + 1) return null;
|
||||
const first = normalizeWireTarget(raw.first);
|
||||
const second = normalizeWireTarget(raw.second);
|
||||
const pair = comparison.normalizePair([first, second]);
|
||||
if (!pair) return null;
|
||||
const signature = pair.map(comparison.targetKey).join("\u001f");
|
||||
if (!signature || signatures.has(signature)) return null;
|
||||
signatures.add(signature);
|
||||
pairs.push(Object.freeze({ rowNumber: index + 1, pair }));
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
requestId: value.requestId,
|
||||
sourceSha256: value.sourceSha256,
|
||||
sourceRowCount: value.sourceRowCount,
|
||||
retrievedAt: value.retrievedAt,
|
||||
pairs: Object.freeze(pairs)
|
||||
});
|
||||
}
|
||||
|
||||
function pairSignature(value) {
|
||||
const pair = comparison.normalizePair(value);
|
||||
return pair ? pair.map(comparison.targetKey).join("\u001f") : null;
|
||||
}
|
||||
|
||||
function mergeImport(currentValue, importValue) {
|
||||
const current = comparison.normalizePairList(currentValue);
|
||||
if (!importValue || !Array.isArray(importValue.pairs) ||
|
||||
!digestPattern.test(String(importValue.sourceSha256 || "")) ||
|
||||
!Number.isInteger(importValue.sourceRowCount) ||
|
||||
importValue.sourceRowCount !== importValue.pairs.length ||
|
||||
importValue.sourceRowCount < 1 || importValue.sourceRowCount > MAXIMUM_PAIRS ||
|
||||
current.pairs.length + importValue.pairs.length > MAXIMUM_PAIRS) {
|
||||
throw new RangeError("The legacy comparison import cannot fit the saved-pair schema.");
|
||||
}
|
||||
|
||||
const ids = new Set(current.pairs.map(record => record.id));
|
||||
const signatures = new Set(current.pairs.map(record => pairSignature(record.pair)));
|
||||
let merged = current;
|
||||
for (let index = 0; index < importValue.pairs.length; index += 1) {
|
||||
const imported = importValue.pairs[index];
|
||||
if (imported?.rowNumber !== index + 1) {
|
||||
throw new TypeError("The legacy comparison import row order is invalid.");
|
||||
}
|
||||
const signature = pairSignature(imported?.pair);
|
||||
if (!signature || signatures.has(signature)) {
|
||||
throw new RangeError("The legacy comparison import contains an existing pair.");
|
||||
}
|
||||
|
||||
const id = `legacy-compare-${importValue.sourceSha256.slice(0, 12).toLocaleLowerCase("en-US")}-${imported.rowNumber}`;
|
||||
if (ids.has(id)) {
|
||||
throw new RangeError("The legacy comparison import id already exists.");
|
||||
}
|
||||
|
||||
const record = comparison.createPairRecord(id, imported.pair);
|
||||
if (!record) throw new TypeError("The legacy comparison import pair is invalid.");
|
||||
merged = comparison.appendPair(merged, record);
|
||||
ids.add(id);
|
||||
signatures.add(signature);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function createMarker(importValue, importedAt = new Date().toISOString()) {
|
||||
if (!importValue || !digestPattern.test(String(importValue.sourceSha256 || "")) ||
|
||||
!Number.isInteger(importValue.sourceRowCount) || importValue.sourceRowCount < 1 ||
|
||||
importValue.sourceRowCount > MAXIMUM_PAIRS ||
|
||||
typeof importedAt !== "string" || !isoDatePattern.test(importedAt) ||
|
||||
!Number.isFinite(Date.parse(importedAt))) return null;
|
||||
return Object.freeze({
|
||||
schemaVersion: IMPORT_SCHEMA_VERSION,
|
||||
sourceSha256: importValue.sourceSha256,
|
||||
importedPairCount: importValue.sourceRowCount,
|
||||
importedAt
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeMarker(value) {
|
||||
if (!hasOnlyKeys(value, ["schemaVersion", "sourceSha256", "importedPairCount", "importedAt"]) ||
|
||||
value.schemaVersion !== IMPORT_SCHEMA_VERSION ||
|
||||
!digestPattern.test(String(value.sourceSha256 || "")) ||
|
||||
!Number.isInteger(value.importedPairCount) || value.importedPairCount < 1 ||
|
||||
value.importedPairCount > MAXIMUM_PAIRS ||
|
||||
typeof value.importedAt !== "string" || !isoDatePattern.test(value.importedAt) ||
|
||||
!Number.isFinite(Date.parse(value.importedAt))) return null;
|
||||
return Object.freeze({
|
||||
schemaVersion: IMPORT_SCHEMA_VERSION,
|
||||
sourceSha256: value.sourceSha256,
|
||||
importedPairCount: value.importedPairCount,
|
||||
importedAt: value.importedAt
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeExactPairList(value) {
|
||||
if (!hasOnlyKeys(value, ["schemaVersion", "pairs"]) || !Array.isArray(value.pairs) ||
|
||||
value.pairs.length > MAXIMUM_PAIRS || value.pairs.some(record =>
|
||||
!hasOnlyKeys(record, ["id", "pair"]))) return null;
|
||||
const normalized = comparison.normalizePairList(value);
|
||||
if (normalized.pairs.length !== value.pairs.length ||
|
||||
normalized.pairs.some((record, index) => record.id !== value.pairs[index].id)) return null;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeStorageEnvelope(value) {
|
||||
if (!hasOnlyKeys(value, ["schemaVersion", "pairList", "legacyImportMarker"]) ||
|
||||
value.schemaVersion !== STORAGE_ENVELOPE_SCHEMA_VERSION) return null;
|
||||
const pairList = normalizeExactPairList(value.pairList);
|
||||
const marker = value.legacyImportMarker === null
|
||||
? null
|
||||
: normalizeMarker(value.legacyImportMarker);
|
||||
if (!pairList || (value.legacyImportMarker !== null && !marker) ||
|
||||
(!marker && pairList.pairs.some(record => importedRecordIdPattern.test(record.id)))) {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: STORAGE_ENVELOPE_SCHEMA_VERSION,
|
||||
pairList,
|
||||
legacyImportMarker: marker
|
||||
});
|
||||
}
|
||||
|
||||
function createStorageEnvelope(pairList, marker = null) {
|
||||
return normalizeStorageEnvelope({
|
||||
schemaVersion: STORAGE_ENVELOPE_SCHEMA_VERSION,
|
||||
pairList,
|
||||
legacyImportMarker: marker
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: IMPORT_SCHEMA_VERSION,
|
||||
storageEnvelopeSchemaVersion: STORAGE_ENVELOPE_SCHEMA_VERSION,
|
||||
maximumPairs: MAXIMUM_PAIRS,
|
||||
normalizeImportResponse,
|
||||
mergeImport,
|
||||
createMarker,
|
||||
normalizeMarker,
|
||||
createStorageEnvelope,
|
||||
normalizeStorageEnvelope
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user