axxerion-api/modules/add-data.js

222 lines
6.1 KiB
JavaScript

const query = require("../db.js");
const WORK_ORDER_TIMESTAMP_FIELDS = new Map([
["Request date", "request_date"],
["Request date 2", "request_date_2"],
["Created", "created"],
["Start", "start"],
["WO Accepted By Vendor", "wo_accepted_by_vendor"],
["Start Work", "start_work"],
["Downtime start date", "downtime_start_date"],
["Downtime end", "downtime_end"],
["Note closed time", "note_closed_time"],
["End Work", "end_work"],
["End", "end"],
["Closed", "closed"],
]);
const WORK_ORDER_FIELD_ALIASES = new Map([
["Address", "address"],
["Assignee", "assignee"],
["Assignee profile", "assignee_profile"],
["Bookmark", "bookmark"],
["Category", "category"],
["Contact", "contact"],
["District", "district"],
["Division manager", "division_manager"],
["Downtime Cat.", "downtime_cat"],
["Downtime?", "downtime"],
["From", "from_requestor"],
["Labor", "labor_amount"],
["Qty. Labor Hours", "labor_hours"],
["Location", "location"],
["Material", "material_amount"],
["NTE Amt. $", "nte_amount"],
["NTE Amount", "nte_amount_2"],
["Part of", "part_of"],
["Priority", "priority"],
["mainProblem", "problem"],
["Problem Type", "problemtype"],
["Property", "property"],
["Reference", "reference"],
["Site #", "site_number"],
["State", "state"],
["Status", "status"],
["Subject", "subject"],
["Total", "total_amount"],
["Travel", "travel_amount"],
["Travel count", "travel_count"],
["Type", "type"],
["VAT", "vat"],
["Vendor Response Time", "vendor_response_time"],
]);
function normalizeIdentifier(value) {
return String(value)
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/[^a-zA-Z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
.replace(/_+/g, "_")
.toLowerCase();
}
function quoteIdentifier(value) {
return `"${String(value).replace(/"/g, "\"\"")}"`;
}
function quoteLiteral(value) {
return `'${String(value).replace(/'/g, "''")}'`;
}
function getColumnAlias(key) {
return WORK_ORDER_TIMESTAMP_FIELDS.get(key)
|| WORK_ORDER_FIELD_ALIASES.get(key)
|| normalizeIdentifier(key);
}
function buildTimestampExpression(tableName, key, alias) {
return `
CASE
WHEN ((${tableName}.data ->> ${quoteLiteral(key)}) = ''::text) THEN NULL::timestamp with time zone
ELSE to_timestamp((${tableName}.data ->> ${quoteLiteral(key)}), 'YYYY-MM-DD"T"HH24:MI:SS'::text)
END AS ${quoteIdentifier(alias)}`;
}
function buildTextExpression(tableName, key, alias) {
return `(${tableName}.data ->> ${quoteLiteral(key)}) AS ${quoteIdentifier(alias)}`;
}
async function ensureTableExists(tableName) {
await query(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id text primary key,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
data jsonb not null
)`);
await query(`
DROP TRIGGER IF EXISTS set_update_${tableName} ON ${tableName}`);
await query(`
CREATE TRIGGER set_update_${tableName}
BEFORE UPDATE ON ${tableName}
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp()`);
}
async function getFlattenedKeys(tableName) {
const result = await query(`
SELECT DISTINCT key
FROM ${tableName},
LATERAL jsonb_object_keys(data) AS key
ORDER BY key`);
return result.rows
.map((row) => row.key)
.filter((key) => !["id", "created_at", "updated_at", "data"].includes(key));
}
async function refreshFlattenedView(tableName, viewName) {
const keys = await getFlattenedKeys(tableName);
const seenAliases = new Set(["id", "created_at", "updated_at", "data"]);
const selectFragments = [
`${tableName}.id`,
`${tableName}.created_at`,
`${tableName}.updated_at`,
];
for (const key of keys) {
let alias = getColumnAlias(key);
if (!alias) {
continue;
}
let suffix = 2;
while (seenAliases.has(alias)) {
alias = `${alias}_${suffix}`;
suffix += 1;
}
seenAliases.add(alias);
const expression = WORK_ORDER_TIMESTAMP_FIELDS.has(key)
? buildTimestampExpression(tableName, key, alias)
: buildTextExpression(tableName, key, alias);
selectFragments.push(expression);
}
await query(`
DROP VIEW IF EXISTS ${viewName}`);
await query(`
CREATE VIEW ${viewName} AS
SELECT ${selectFragments.join(",\n ")}
FROM ${tableName}`);
}
const addData = async (req, res) => {
try {
const client = req.params.client;
const { tableName, data } = req.body;
if (typeof tableName !== "string" || !tableName.trim()) {
return res.status(400).json({ error: "tableName is required." });
}
if (!Array.isArray(data)) {
return res.status(400).json({ error: "data must be an array." });
}
const normalizedClient = normalizeIdentifier(client);
const normalizedTableName = normalizeIdentifier(tableName);
if (!normalizedClient || !normalizedTableName) {
return res.status(400).json({ error: "Invalid client or tableName." });
}
const dBtableName = `${normalizedClient}_${normalizedTableName}`;
const flattenedViewName = `${normalizedClient}_flattened_${normalizedTableName}`;
const insertQuery = `
INSERT INTO ${dBtableName} (id, data) VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET data = $2`;
for (const item of data) {
if (!item || typeof item !== "object" || item.id == null) {
return res.status(400).json({ error: "Each data item must be an object with an id." });
}
}
await query("BEGIN");
try {
await ensureTableExists(dBtableName);
for (const item of data) {
await query(insertQuery, [String(item.id), item]);
}
await refreshFlattenedView(dBtableName, flattenedViewName);
await query("COMMIT");
} catch (err) {
await query("ROLLBACK");
throw err;
}
res.status(200).json({
message: "Data added successfully",
tableName: dBtableName,
viewName: flattenedViewName,
});
} catch (err) {
console.error("Error handling the request:", err);
res
.status(500)
.json({ error: "An error occurred while processing the request" });
}
};
module.exports = addData;