201 lines
6.2 KiB
JavaScript
201 lines
6.2 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import {
|
|
ensureParentDir,
|
|
resolveOutputFilePath,
|
|
} from "./runtime-paths.mjs";
|
|
|
|
const NULL_SENTINEL = "__CODEX_NULL__";
|
|
const MONETARY_FIELD_KEYS = new Set(["bookingValue", "opportunityValue", "workloadAmount", "pipelineAmount"]);
|
|
const THOUSANDS_DISPLAY_KEYS = new Set([
|
|
...MONETARY_FIELD_KEYS,
|
|
"totalBookingValue",
|
|
"totalOpportunityValue",
|
|
"totalWorkloadAmount",
|
|
"totalPipelineAmount",
|
|
]);
|
|
const THOUSANDS_DISPLAY_SOURCE_FIELDS = new Set(MONETARY_FIELD_KEYS);
|
|
const ORACLE_MONTHS = {
|
|
JAN: "01",
|
|
FEB: "02",
|
|
MAR: "03",
|
|
APR: "04",
|
|
MAY: "05",
|
|
JUN: "06",
|
|
JUL: "07",
|
|
AUG: "08",
|
|
SEP: "09",
|
|
OCT: "10",
|
|
NOV: "11",
|
|
DEC: "12",
|
|
};
|
|
|
|
export function buildWarnings({ requestedLimit, recordsCount, customSql, customSqlColumnsResolved = false }) {
|
|
const warnings = [];
|
|
if (requestedLimit > 0 && recordsCount >= requestedLimit) {
|
|
warnings.push(
|
|
`The query returned ${recordsCount} rows, which matches the requested limit (${requestedLimit}). Results may be truncated; rerun with a higher --limit if you need a complete extract.`
|
|
);
|
|
}
|
|
if (customSql && !customSqlColumnsResolved) {
|
|
warnings.push("Custom SQL records are returned as raw row arrays because the Oracle DV response does not guarantee stable field aliases.");
|
|
}
|
|
return warnings;
|
|
}
|
|
|
|
export function parseNumericValue(value) {
|
|
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
const normalized = String(value ?? "")
|
|
.replace(/,/g, "")
|
|
.trim();
|
|
if (!normalized) return 0;
|
|
const parsed = Number(normalized);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function isLikelyMonetaryFieldName(fieldName) {
|
|
return /booking|workload|pipeline|opportunityvalue/i.test(String(fieldName || ""));
|
|
}
|
|
|
|
function formatThousandsDisplayValue(value) {
|
|
const thousands = Math.trunc(parseNumericValue(value) / 1000);
|
|
const grouped = String(thousands).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
return `${grouped}k`;
|
|
}
|
|
|
|
function formatDisplaySection(node, displayKeys = THOUSANDS_DISPLAY_KEYS) {
|
|
if (Array.isArray(node)) {
|
|
return node.map((item) => formatDisplaySection(item, displayKeys));
|
|
}
|
|
if (!node || typeof node !== "object") {
|
|
return node;
|
|
}
|
|
return Object.fromEntries(
|
|
Object.entries(node).map(([key, value]) => {
|
|
if ((displayKeys.has(key) || isLikelyMonetaryFieldName(key)) && value !== null && value !== undefined && value !== "") {
|
|
return [key, formatThousandsDisplayValue(value)];
|
|
}
|
|
return [key, formatDisplaySection(value, displayKeys)];
|
|
})
|
|
);
|
|
}
|
|
|
|
function collectThousandsDisplayKeys(payload) {
|
|
const displayKeys = new Set(THOUSANDS_DISPLAY_KEYS);
|
|
for (const columnName of payload?.columns || []) {
|
|
if (MONETARY_FIELD_KEYS.has(columnName) || isLikelyMonetaryFieldName(columnName)) {
|
|
displayKeys.add(columnName);
|
|
}
|
|
}
|
|
const querySpec = payload?.request?.query;
|
|
if (querySpec && typeof querySpec === "object") {
|
|
for (const item of [...(querySpec.select || []), ...(querySpec.measure || [])]) {
|
|
if (!item || typeof item !== "object") continue;
|
|
if (!THOUSANDS_DISPLAY_SOURCE_FIELDS.has(item.fieldName)) continue;
|
|
if (item.outputName) displayKeys.add(item.outputName);
|
|
}
|
|
}
|
|
|
|
for (const inferredColumn of payload?.request?.inferredColumns || []) {
|
|
if (MONETARY_FIELD_KEYS.has(inferredColumn) || isLikelyMonetaryFieldName(inferredColumn)) {
|
|
displayKeys.add(inferredColumn);
|
|
}
|
|
}
|
|
|
|
return displayKeys;
|
|
}
|
|
|
|
function rowsPulledFromApi(result) {
|
|
return Array.isArray(result?.aResults) ? result.aResults.length : 0;
|
|
}
|
|
|
|
function buildApiQueryMetric({
|
|
label,
|
|
result = null,
|
|
rowsReturned = null,
|
|
source = null,
|
|
countedInTotal = true,
|
|
} = {}) {
|
|
const resolvedRowsReturned = Number.isFinite(rowsReturned) ? rowsReturned : rowsPulledFromApi(result);
|
|
return {
|
|
label: label || "query",
|
|
rowsPulledFromApi: resolvedRowsReturned,
|
|
source: source || null,
|
|
countedInTotal: countedInTotal !== false,
|
|
};
|
|
}
|
|
|
|
export function buildApiMetrics(entries = []) {
|
|
const queries = entries
|
|
.filter(Boolean)
|
|
.map((entry) =>
|
|
buildApiQueryMetric({
|
|
label: entry.label,
|
|
result: entry.result,
|
|
rowsReturned: entry.rowsReturned,
|
|
source: entry.source,
|
|
countedInTotal: entry.countedInTotal,
|
|
})
|
|
);
|
|
const totalRowsPulledFromApi = queries.reduce(
|
|
(sum, query) => sum + (query.countedInTotal ? query.rowsPulledFromApi : 0),
|
|
0
|
|
);
|
|
return {
|
|
totalRowsPulledFromApi,
|
|
queries,
|
|
explanation: `Raw rows pulled from API: ${totalRowsPulledFromApi}.`,
|
|
};
|
|
}
|
|
|
|
export function preparePayloadForOutput(payload) {
|
|
const displayKeys = collectThousandsDisplayKeys(payload);
|
|
return {
|
|
...payload,
|
|
apiMetrics: payload?.apiMetrics || buildApiMetrics(),
|
|
...(Object.prototype.hasOwnProperty.call(payload, "records")
|
|
? { records: formatDisplaySection(payload.records, displayKeys) }
|
|
: {}),
|
|
...(payload.summary && typeof payload.summary === "object"
|
|
? { summary: formatDisplaySection(payload.summary, displayKeys) }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function normalizeOracleDateString(value) {
|
|
if (typeof value !== "string") return value;
|
|
const match = /^(\d{2})-([A-Z]{3})-(\d{2}|\d{4})$/.exec(value.trim().toUpperCase());
|
|
if (!match) return value;
|
|
const [, day, monthToken, yearToken] = match;
|
|
const month = ORACLE_MONTHS[monthToken];
|
|
if (!month) return value;
|
|
const year =
|
|
yearToken.length === 4
|
|
? yearToken
|
|
: Number(yearToken) >= 70
|
|
? `19${yearToken}`
|
|
: `20${yearToken}`;
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
export function normalizeCell(value) {
|
|
if (value === NULL_SENTINEL) return null;
|
|
return normalizeOracleDateString(value ?? null);
|
|
}
|
|
|
|
export function mapRows(fields, rows) {
|
|
return rows.map((row) =>
|
|
Object.fromEntries(fields.map((field, index) => [field, normalizeCell(row[index])]))
|
|
);
|
|
}
|
|
|
|
export function emitJson(args, payload, runtimeHome) {
|
|
const json = JSON.stringify(preparePayloadForOutput(payload), null, 2);
|
|
const outputFilePath = resolveOutputFilePath(args["output-file"], runtimeHome);
|
|
if (outputFilePath) {
|
|
return ensureParentDir(outputFilePath)
|
|
.then(() => fs.writeFile(outputFilePath, `${json}\n`, "utf8"))
|
|
.then(() => json);
|
|
}
|
|
return Promise.resolve(json);
|
|
}
|