All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m16s
178 lines
7.1 KiB
JavaScript
178 lines
7.1 KiB
JavaScript
const { pool } = require('../db');
|
|
|
|
const SAMPLE_LIMIT = 50;
|
|
|
|
const sampleSpecs = {
|
|
catalog_categories: {
|
|
columns: ['id', 'name', 'description', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
catalog_products: {
|
|
columns: ['id', 'type', 'sku', 'name', 'category_id', 'composition', 'gramature', 'material_yield', 'width_cm', 'color', 'subcategory', 'sizes', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
consumption_references: {
|
|
columns: ['id', 'product_id', 'material_product_id', 'color', 'general_yield', 'size_yields', 'size_areas', 'gramature', 'efficiency_percent', 'rib_g_per_piece', 'material_cost_per_kg', 'consumption_quantity', 'consumption_unit', 'source', 'last_production_order_id', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
cutting_family_rules: {
|
|
columns: ['family_key', 'units_per_roll', 'updated_at'],
|
|
orderBy: ['family_key']
|
|
},
|
|
cutting_product_overrides: {
|
|
columns: ['product_id', 'family_key', 'color', 'size', 'product_type', 'planning_notes', 'updated_at'],
|
|
orderBy: ['updated_at', 'product_id']
|
|
},
|
|
orders: {
|
|
columns: ['id', 'pedido_id', 'data_pedido', 'data_pedido_date', 'valor_pedido', 'produto_id', 'produto_descricao', 'quantidade', 'valor_unitario', 'id_vendedor', 'nome_vendedor', 'marketplace', 'canal_venda', 'numero_ecommerce', 'created_at'],
|
|
orderBy: ['data_pedido_date', 'created_at', 'id']
|
|
},
|
|
production_order_markers: {
|
|
columns: ['id', 'production_order_id', 'label', 'color', 'created_at'],
|
|
orderBy: ['created_at', 'id']
|
|
},
|
|
production_order_components: {
|
|
columns: ['id', 'production_order_id', 'component_tiny_id', 'component_sku', 'component_name', 'quantity_per_unit', 'total_quantity', 'unit', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
production_order_steps: {
|
|
columns: ['id', 'production_order_id', 'step_number', 'name', 'start_date', 'end_date', 'status', 'color', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
production_orders: {
|
|
columns: ['id', 'tiny_id', 'number', 'status', 'order_reference', 'issue_date', 'expected_date', 'product_sku', 'product_description', 'quantity', 'unit', 'integration_status', 'notes', 'supplier', 'lot_code', 'roll_quantity', 'fabric_kg', 'rib_kg', 'yield_pieces_per_kg', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
stock: {
|
|
columns: ['produto_id', 'nome', 'saldo', 'delta_estoque', 'updated_at'],
|
|
orderBy: ['updated_at', 'produto_id']
|
|
},
|
|
stock_campaign_queue: {
|
|
columns: ['id', 'base_product_name', 'produto_id', 'nome', 'saldo', 'delta_estoque', 'status', 'attempts', 'last_error', 'created_at', 'updated_at', 'sent_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
supply_fabric_plans: {
|
|
columns: ['id', 'material', 'color', 'quantity_kg', 'supplier', 'priority', 'status', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
supply_movements: {
|
|
columns: ['id', 'receipt_id', 'lot_id', 'type', 'category', 'product', 'quantity', 'unit', 'reason', 'created_at'],
|
|
orderBy: ['created_at', 'id']
|
|
},
|
|
supply_receipts: {
|
|
columns: ['id', 'category', 'product', 'quantity', 'unit', 'supplier', 'invoice', 'notes', 'status', 'created_at', 'updated_at', 'approved_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
},
|
|
supply_stock_lots: {
|
|
columns: ['id', 'receipt_id', 'category', 'product', 'quantity', 'unit', 'supplier', 'invoice', 'status', 'created_at', 'updated_at'],
|
|
orderBy: ['updated_at', 'created_at', 'id']
|
|
}
|
|
};
|
|
|
|
const quoteIdentifier = (identifier) => {
|
|
if (!/^[a-z_][a-z0-9_]*$/.test(identifier)) {
|
|
throw new Error(`Unsafe database identifier: ${identifier}`);
|
|
}
|
|
|
|
return `"${identifier}"`;
|
|
};
|
|
|
|
const listPublicTables = async () => {
|
|
const result = await pool.query(`
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_type = 'BASE TABLE'
|
|
ORDER BY table_name
|
|
`);
|
|
|
|
return result.rows.map(row => row.table_name);
|
|
};
|
|
|
|
const listPublicColumns = async () => {
|
|
const result = await pool.query(`
|
|
SELECT table_name, column_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name, ordinal_position
|
|
`);
|
|
|
|
return result.rows.reduce((columnsByTable, row) => {
|
|
if (!columnsByTable[row.table_name]) columnsByTable[row.table_name] = [];
|
|
columnsByTable[row.table_name].push(row.column_name);
|
|
return columnsByTable;
|
|
}, {});
|
|
};
|
|
|
|
const countTableRows = async (tableName) => {
|
|
const result = await pool.query(`SELECT COUNT(*)::int AS count FROM ${quoteIdentifier(tableName)}`);
|
|
return result.rows[0]?.count || 0;
|
|
};
|
|
|
|
const buildOrderClause = (spec, availableColumns) => {
|
|
const orderColumns = spec.orderBy.filter(column => availableColumns.includes(column));
|
|
if (!orderColumns.length) return '';
|
|
|
|
const clauses = orderColumns.map(column => {
|
|
const direction = column === 'family_key' || column === 'product_id' || column === 'produto_id' ? 'ASC' : 'DESC';
|
|
return `${quoteIdentifier(column)} ${direction}`;
|
|
});
|
|
|
|
return ` ORDER BY ${clauses.join(', ')}`;
|
|
};
|
|
|
|
const sampleTableRows = async (tableName, availableColumns) => {
|
|
const spec = sampleSpecs[tableName];
|
|
if (!spec) return null;
|
|
|
|
const selectedColumns = spec.columns.filter(column => availableColumns.includes(column));
|
|
if (!selectedColumns.length) return null;
|
|
|
|
const selectClause = selectedColumns.map(quoteIdentifier).join(', ');
|
|
const orderClause = buildOrderClause(spec, availableColumns);
|
|
const result = await pool.query(
|
|
`SELECT ${selectClause} FROM ${quoteIdentifier(tableName)}${orderClause} LIMIT $1`,
|
|
[SAMPLE_LIMIT]
|
|
);
|
|
|
|
return result.rows;
|
|
};
|
|
|
|
const buildDatabaseDiagnostic = async (user) => {
|
|
const [tables, columnsByTable] = await Promise.all([
|
|
listPublicTables(),
|
|
listPublicColumns()
|
|
]);
|
|
|
|
const counts = {};
|
|
const samples = {};
|
|
|
|
for (const tableName of tables) {
|
|
counts[tableName] = await countTableRows(tableName);
|
|
|
|
const sampleRows = await sampleTableRows(tableName, columnsByTable[tableName] || []);
|
|
if (sampleRows) samples[tableName] = sampleRows;
|
|
}
|
|
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
generatedBy: {
|
|
role: user?.role || null,
|
|
userId: user?.userId || null
|
|
},
|
|
sampleLimit: SAMPLE_LIMIT,
|
|
privacy: {
|
|
countsIncludeAllPublicTables: true,
|
|
samplesExcludeTables: ['app_users', 'client_identity_tokens'],
|
|
ordersSampleExcludesCustomerNameAndPhone: true,
|
|
productionOrdersSampleExcludesTinyPayload: true
|
|
},
|
|
counts,
|
|
samples
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
buildDatabaseDiagnostic
|
|
};
|