Primeiro Commit v0.beta1
This commit is contained in:
200
scripts/lib/output-contract.mjs
Normal file
200
scripts/lib/output-contract.mjs
Normal file
@@ -0,0 +1,200 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user