2024-11-20 19:41:09 +00:00
|
|
|
const format = require("pg-format");
|
|
|
|
const XLSX = require('xlsx');
|
|
|
|
|
2024-11-20 22:45:26 +00:00
|
|
|
function deduceType(cells) {
|
|
|
|
if (!cells || cells.length === 0) return 'text';
|
|
|
|
|
|
|
|
const nonEmptyCells = cells.filter(cell => cell && cell.v != null);
|
|
|
|
if (nonEmptyCells.length === 0) return 'text';
|
|
|
|
|
|
|
|
// Check for dates by looking at both cell type and formatted value
|
2024-11-20 23:33:20 +00:00
|
|
|
const isDateCell = cell => cell?.t === 'd' || (cell?.t === 'n' && cell.w && /\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{4}|\d{2}-[A-Za-z]{3}-\d{4}|[A-Za-z]{3}-\d{2}|\d{1,2}-[A-Za-z]{3}/.test(cell.w));
|
2024-11-20 19:41:09 +00:00
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
if (nonEmptyCells.some(isDateCell)) { return 'date'; }
|
2024-11-20 22:45:26 +00:00
|
|
|
|
|
|
|
// Check for booleans
|
|
|
|
const allBooleans = nonEmptyCells.every(cell => cell.t === 'b');
|
|
|
|
if (allBooleans) { return 'boolean'; }
|
|
|
|
|
|
|
|
// Check for numbers
|
2024-11-20 23:33:20 +00:00
|
|
|
const allNumbers = nonEmptyCells.every(cell => cell.t === 'n' || (cell.t === 's' && !isNaN(cell.v.replace(/[,$\s%()]/g, ''))));
|
2024-11-20 22:45:26 +00:00
|
|
|
|
|
|
|
if (allNumbers) {
|
|
|
|
const numbers = nonEmptyCells.map(cell => {
|
|
|
|
if (cell.t === 'n') return cell.v;
|
|
|
|
return parseFloat(cell.v.replace(/[,$\s%()]/g, ''));
|
|
|
|
});
|
2024-11-20 19:41:09 +00:00
|
|
|
|
2024-11-20 22:45:26 +00:00
|
|
|
const needsPrecision = numbers.some(num => {
|
|
|
|
const str = num.toString();
|
|
|
|
return str.includes('e') ||
|
|
|
|
(str.includes('.') && str.split('.')[1].length > 6) ||
|
|
|
|
Math.abs(num) > 1e15;
|
|
|
|
});
|
2024-11-20 19:41:09 +00:00
|
|
|
|
2024-11-20 22:45:26 +00:00
|
|
|
return needsPrecision ? 'numeric' : 'double precision';
|
2024-11-20 19:41:09 +00:00
|
|
|
}
|
2024-11-20 22:45:26 +00:00
|
|
|
return 'text'; // default to string type
|
2024-11-20 19:41:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function parseValue(cell, type) {
|
2024-11-20 22:45:26 +00:00
|
|
|
if (!cell || cell.v == null) return null;
|
2024-11-20 19:41:09 +00:00
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'date':
|
2024-11-20 23:33:20 +00:00
|
|
|
if (cell.t === 'd') { return cell.v.toISOString().split('T')[0]; }
|
2024-11-20 22:45:26 +00:00
|
|
|
if (cell.t === 'n') {
|
|
|
|
const date = new Date((cell.v - 25569) * 86400 * 1000);
|
|
|
|
return date.toISOString().split('T')[0];
|
|
|
|
}
|
2024-11-20 19:41:09 +00:00
|
|
|
return null;
|
2024-11-20 22:45:26 +00:00
|
|
|
|
|
|
|
case 'numeric':
|
2024-11-20 19:41:09 +00:00
|
|
|
case 'double precision':
|
|
|
|
if (cell.t === 'n') return cell.v;
|
2024-11-20 22:45:26 +00:00
|
|
|
if (cell.t === 's') {
|
|
|
|
const cleaned = cell.v.replace(/[,$\s%()]/g, '');
|
|
|
|
if (!isNaN(cleaned)) return parseFloat(cleaned);
|
|
|
|
}
|
2024-11-20 19:41:09 +00:00
|
|
|
return null;
|
2024-11-20 22:45:26 +00:00
|
|
|
|
2024-11-20 19:41:09 +00:00
|
|
|
case 'boolean':
|
|
|
|
return cell.t === 'b' ? cell.v : null;
|
2024-11-20 22:45:26 +00:00
|
|
|
|
2024-11-20 19:41:09 +00:00
|
|
|
default:
|
|
|
|
return String(cell.v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
/* create table and load data given a worksheet and a PostgreSQL client */
|
|
|
|
async function sheet_to_pg_table(client, worksheet, tableName) {
|
2024-11-20 19:41:09 +00:00
|
|
|
if (!worksheet['!ref']) return;
|
|
|
|
|
|
|
|
const range = XLSX.utils.decode_range(worksheet['!ref']);
|
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
/* Extract headers from first row, clean names for PostgreSQL */
|
2024-11-20 19:41:09 +00:00
|
|
|
const headers = [];
|
|
|
|
for (let col = range.s.c; col <= range.e.c; col++) {
|
|
|
|
const cellAddress = XLSX.utils.encode_cell({ r: range.s.r, c: col });
|
|
|
|
const cell = worksheet[cellAddress];
|
|
|
|
const headerValue = cell ? String(cell.v).replace(/[^a-zA-Z0-9_]/g, '_') : `column_${col + 1}`;
|
|
|
|
headers.push(headerValue.toLowerCase());
|
|
|
|
}
|
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
/* Group cell values by column for type deduction */
|
2024-11-20 19:41:09 +00:00
|
|
|
const columnValues = headers.map(() => []);
|
|
|
|
for (let row = range.s.r + 1; row <= range.e.r; row++) {
|
|
|
|
for (let col = range.s.c; col <= range.e.c; col++) {
|
|
|
|
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
|
|
|
|
const cell = worksheet[cellAddress];
|
|
|
|
columnValues[col].push(cell);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
/* Deduce PostgreSQL type for each column */
|
2024-11-20 19:41:09 +00:00
|
|
|
const types = {};
|
|
|
|
headers.forEach((header, idx) => {
|
|
|
|
types[header] = deduceType(columnValues[idx]);
|
|
|
|
});
|
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
/* Delete table if it exists in the DB */
|
2024-11-20 19:41:09 +00:00
|
|
|
await client.query(format('DROP TABLE IF EXISTS %I', tableName));
|
2024-11-20 23:33:20 +00:00
|
|
|
|
|
|
|
/* Create table */
|
2024-11-20 19:41:09 +00:00
|
|
|
const createTableSQL = format(
|
|
|
|
'CREATE TABLE %I (%s)',
|
|
|
|
tableName,
|
|
|
|
headers.map(header => format('%I %s', header, types[header])).join(', ')
|
|
|
|
);
|
|
|
|
await client.query(createTableSQL);
|
|
|
|
|
2024-11-20 23:33:20 +00:00
|
|
|
/* Insert data row by row */
|
2024-11-20 19:41:09 +00:00
|
|
|
for (let row = range.s.r + 1; row <= range.e.r; row++) {
|
|
|
|
const values = headers.map((header, col) => {
|
|
|
|
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
|
|
|
|
const cell = worksheet[cellAddress];
|
|
|
|
return parseValue(cell, types[header]);
|
|
|
|
});
|
|
|
|
|
|
|
|
const insertSQL = format(
|
|
|
|
'INSERT INTO %I (%s) VALUES (%s)',
|
|
|
|
tableName,
|
|
|
|
headers.map(h => format('%I', h)).join(', '),
|
|
|
|
values.map(() => '%L').join(', ')
|
|
|
|
);
|
|
|
|
await client.query(format(insertSQL, ...values));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = { sheet_to_pg_table };
|