2 Commits

Author SHA1 Message Date
f2a5c06005 Fixes for re-running reset scripts 2025-02-13 10:25:04 -05:00
fb9f959fe5 Update schemas and reset scripts 2025-02-12 16:14:25 -05:00
10 changed files with 1761 additions and 1158 deletions

View File

@@ -1,150 +1,154 @@
-- Configuration tables schema -- Configuration tables schema
-- Stock threshold configurations -- Stock threshold configurations
CREATE TABLE IF NOT EXISTS stock_thresholds ( CREATE TABLE stock_thresholds (
id INT NOT NULL, id INTEGER NOT NULL,
category_id BIGINT, -- NULL means default/global threshold category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors vendor VARCHAR(100), -- NULL means applies to all vendors
critical_days INT NOT NULL DEFAULT 7, critical_days INTEGER NOT NULL DEFAULT 7,
reorder_days INT NOT NULL DEFAULT 14, reorder_days INTEGER NOT NULL DEFAULT 14,
overstock_days INT NOT NULL DEFAULT 90, overstock_days INTEGER NOT NULL DEFAULT 90,
low_stock_threshold INT NOT NULL DEFAULT 5, low_stock_threshold INTEGER NOT NULL DEFAULT 5,
min_reorder_quantity INT NOT NULL DEFAULT 1, min_reorder_quantity INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor), UNIQUE (category_id, vendor)
INDEX idx_st_metrics (category_id, vendor)
); );
CREATE INDEX idx_st_metrics ON stock_thresholds(category_id, vendor);
-- Lead time threshold configurations -- Lead time threshold configurations
CREATE TABLE IF NOT EXISTS lead_time_thresholds ( CREATE TABLE lead_time_thresholds (
id INT NOT NULL, id INTEGER NOT NULL,
category_id BIGINT, -- NULL means default/global threshold category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors vendor VARCHAR(100), -- NULL means applies to all vendors
target_days INT NOT NULL DEFAULT 14, target_days INTEGER NOT NULL DEFAULT 14,
warning_days INT NOT NULL DEFAULT 21, warning_days INTEGER NOT NULL DEFAULT 21,
critical_days INT NOT NULL DEFAULT 30, critical_days INTEGER NOT NULL DEFAULT 30,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor) UNIQUE (category_id, vendor)
); );
-- Sales velocity window configurations -- Sales velocity window configurations
CREATE TABLE IF NOT EXISTS sales_velocity_config ( CREATE TABLE sales_velocity_config (
id INT NOT NULL, id INTEGER NOT NULL,
category_id BIGINT, -- NULL means default/global threshold category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors vendor VARCHAR(100), -- NULL means applies to all vendors
daily_window_days INT NOT NULL DEFAULT 30, daily_window_days INTEGER NOT NULL DEFAULT 30,
weekly_window_days INT NOT NULL DEFAULT 7, weekly_window_days INTEGER NOT NULL DEFAULT 7,
monthly_window_days INT NOT NULL DEFAULT 90, monthly_window_days INTEGER NOT NULL DEFAULT 90,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor), UNIQUE (category_id, vendor)
INDEX idx_sv_metrics (category_id, vendor)
); );
CREATE INDEX idx_sv_metrics ON sales_velocity_config(category_id, vendor);
-- ABC Classification configurations -- ABC Classification configurations
CREATE TABLE IF NOT EXISTS abc_classification_config ( CREATE TABLE abc_classification_config (
id INT NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
a_threshold DECIMAL(5,2) NOT NULL DEFAULT 20.0, a_threshold DECIMAL(5,2) NOT NULL DEFAULT 20.0,
b_threshold DECIMAL(5,2) NOT NULL DEFAULT 50.0, b_threshold DECIMAL(5,2) NOT NULL DEFAULT 50.0,
classification_period_days INT NOT NULL DEFAULT 90, classification_period_days INTEGER NOT NULL DEFAULT 90,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- Safety stock configurations -- Safety stock configurations
CREATE TABLE IF NOT EXISTS safety_stock_config ( CREATE TABLE safety_stock_config (
id INT NOT NULL, id INTEGER NOT NULL,
category_id BIGINT, -- NULL means default/global threshold category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors vendor VARCHAR(100), -- NULL means applies to all vendors
coverage_days INT NOT NULL DEFAULT 14, coverage_days INTEGER NOT NULL DEFAULT 14,
service_level DECIMAL(5,2) NOT NULL DEFAULT 95.0, service_level DECIMAL(5,2) NOT NULL DEFAULT 95.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor), UNIQUE (category_id, vendor)
INDEX idx_ss_metrics (category_id, vendor)
); );
CREATE INDEX idx_ss_metrics ON safety_stock_config(category_id, vendor);
-- Turnover rate configurations -- Turnover rate configurations
CREATE TABLE IF NOT EXISTS turnover_config ( CREATE TABLE turnover_config (
id INT NOT NULL, id INTEGER NOT NULL,
category_id BIGINT, -- NULL means default/global threshold category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors vendor VARCHAR(100), -- NULL means applies to all vendors
calculation_period_days INT NOT NULL DEFAULT 30, calculation_period_days INTEGER NOT NULL DEFAULT 30,
target_rate DECIMAL(10,2) NOT NULL DEFAULT 1.0, target_rate DECIMAL(10,2) NOT NULL DEFAULT 1.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor) UNIQUE (category_id, vendor)
); );
-- Create table for sales seasonality factors -- Create table for sales seasonality factors
CREATE TABLE IF NOT EXISTS sales_seasonality ( CREATE TABLE sales_seasonality (
month INT NOT NULL, month INTEGER NOT NULL,
seasonality_factor DECIMAL(5,3) DEFAULT 0, seasonality_factor DECIMAL(5,3) DEFAULT 0,
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (month), PRIMARY KEY (month),
CHECK (month BETWEEN 1 AND 12), CONSTRAINT month_range CHECK (month BETWEEN 1 AND 12),
CHECK (seasonality_factor BETWEEN -1.0 AND 1.0) CONSTRAINT seasonality_range CHECK (seasonality_factor BETWEEN -1.0 AND 1.0)
); );
-- Insert default global thresholds if not exists -- Insert default global thresholds
INSERT INTO stock_thresholds (id, category_id, vendor, critical_days, reorder_days, overstock_days) INSERT INTO stock_thresholds (id, category_id, vendor, critical_days, reorder_days, overstock_days)
VALUES (1, NULL, NULL, 7, 14, 90) VALUES (1, NULL, NULL, 7, 14, 90)
ON DUPLICATE KEY UPDATE ON CONFLICT (id) DO UPDATE SET
critical_days = VALUES(critical_days), critical_days = EXCLUDED.critical_days,
reorder_days = VALUES(reorder_days), reorder_days = EXCLUDED.reorder_days,
overstock_days = VALUES(overstock_days); overstock_days = EXCLUDED.overstock_days;
INSERT INTO lead_time_thresholds (id, category_id, vendor, target_days, warning_days, critical_days) INSERT INTO lead_time_thresholds (id, category_id, vendor, target_days, warning_days, critical_days)
VALUES (1, NULL, NULL, 14, 21, 30) VALUES (1, NULL, NULL, 14, 21, 30)
ON DUPLICATE KEY UPDATE ON CONFLICT (id) DO UPDATE SET
target_days = VALUES(target_days), target_days = EXCLUDED.target_days,
warning_days = VALUES(warning_days), warning_days = EXCLUDED.warning_days,
critical_days = VALUES(critical_days); critical_days = EXCLUDED.critical_days;
INSERT INTO sales_velocity_config (id, category_id, vendor, daily_window_days, weekly_window_days, monthly_window_days) INSERT INTO sales_velocity_config (id, category_id, vendor, daily_window_days, weekly_window_days, monthly_window_days)
VALUES (1, NULL, NULL, 30, 7, 90) VALUES (1, NULL, NULL, 30, 7, 90)
ON DUPLICATE KEY UPDATE ON CONFLICT (id) DO UPDATE SET
daily_window_days = VALUES(daily_window_days), daily_window_days = EXCLUDED.daily_window_days,
weekly_window_days = VALUES(weekly_window_days), weekly_window_days = EXCLUDED.weekly_window_days,
monthly_window_days = VALUES(monthly_window_days); monthly_window_days = EXCLUDED.monthly_window_days;
INSERT INTO abc_classification_config (id, a_threshold, b_threshold, classification_period_days) INSERT INTO abc_classification_config (id, a_threshold, b_threshold, classification_period_days)
VALUES (1, 20.0, 50.0, 90) VALUES (1, 20.0, 50.0, 90)
ON DUPLICATE KEY UPDATE ON CONFLICT (id) DO UPDATE SET
a_threshold = VALUES(a_threshold), a_threshold = EXCLUDED.a_threshold,
b_threshold = VALUES(b_threshold), b_threshold = EXCLUDED.b_threshold,
classification_period_days = VALUES(classification_period_days); classification_period_days = EXCLUDED.classification_period_days;
INSERT INTO safety_stock_config (id, category_id, vendor, coverage_days, service_level) INSERT INTO safety_stock_config (id, category_id, vendor, coverage_days, service_level)
VALUES (1, NULL, NULL, 14, 95.0) VALUES (1, NULL, NULL, 14, 95.0)
ON DUPLICATE KEY UPDATE ON CONFLICT (id) DO UPDATE SET
coverage_days = VALUES(coverage_days), coverage_days = EXCLUDED.coverage_days,
service_level = VALUES(service_level); service_level = EXCLUDED.service_level;
INSERT INTO turnover_config (id, category_id, vendor, calculation_period_days, target_rate) INSERT INTO turnover_config (id, category_id, vendor, calculation_period_days, target_rate)
VALUES (1, NULL, NULL, 30, 1.0) VALUES (1, NULL, NULL, 30, 1.0)
ON DUPLICATE KEY UPDATE ON CONFLICT (id) DO UPDATE SET
calculation_period_days = VALUES(calculation_period_days), calculation_period_days = EXCLUDED.calculation_period_days,
target_rate = VALUES(target_rate); target_rate = EXCLUDED.target_rate;
-- Insert default seasonality factors (neutral) -- Insert default seasonality factors (neutral)
INSERT INTO sales_seasonality (month, seasonality_factor) INSERT INTO sales_seasonality (month, seasonality_factor)
VALUES VALUES
(1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0),
(7, 0), (8, 0), (9, 0), (10, 0), (11, 0), (12, 0) (7, 0), (8, 0), (9, 0), (10, 0), (11, 0), (12, 0)
ON DUPLICATE KEY UPDATE last_updated = CURRENT_TIMESTAMP; ON CONFLICT (month) DO UPDATE SET
last_updated = CURRENT_TIMESTAMP;
-- View to show thresholds with category names -- View to show thresholds with category names
CREATE OR REPLACE VIEW stock_thresholds_view AS CREATE OR REPLACE VIEW stock_thresholds_view AS
@@ -153,9 +157,9 @@ SELECT
c.name as category_name, c.name as category_name,
CASE CASE
WHEN st.category_id IS NULL AND st.vendor IS NULL THEN 'Global Default' WHEN st.category_id IS NULL AND st.vendor IS NULL THEN 'Global Default'
WHEN st.category_id IS NULL THEN CONCAT('Vendor: ', st.vendor) WHEN st.category_id IS NULL THEN 'Vendor: ' || st.vendor
WHEN st.vendor IS NULL THEN CONCAT('Category: ', c.name) WHEN st.vendor IS NULL THEN 'Category: ' || c.name
ELSE CONCAT('Category: ', c.name, ' / Vendor: ', st.vendor) ELSE 'Category: ' || c.name || ' / Vendor: ' || st.vendor
END as threshold_scope END as threshold_scope
FROM FROM
stock_thresholds st stock_thresholds st
@@ -171,59 +175,51 @@ ORDER BY
c.name, c.name,
st.vendor; st.vendor;
-- History and status tables
CREATE TABLE IF NOT EXISTS calculate_history ( CREATE TABLE IF NOT EXISTS calculate_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP NULL, end_time TIMESTAMP NULL,
duration_seconds INT, duration_seconds INTEGER,
duration_minutes DECIMAL(10,2) GENERATED ALWAYS AS (duration_seconds / 60.0) STORED, duration_minutes DECIMAL(10,2) GENERATED ALWAYS AS (duration_seconds::decimal / 60.0) STORED,
total_products INT DEFAULT 0, total_products INTEGER DEFAULT 0,
total_orders INT DEFAULT 0, total_orders INTEGER DEFAULT 0,
total_purchase_orders INT DEFAULT 0, total_purchase_orders INTEGER DEFAULT 0,
processed_products INT DEFAULT 0, processed_products INTEGER DEFAULT 0,
processed_orders INT DEFAULT 0, processed_orders INTEGER DEFAULT 0,
processed_purchase_orders INT DEFAULT 0, processed_purchase_orders INTEGER DEFAULT 0,
status ENUM('running', 'completed', 'failed', 'cancelled') DEFAULT 'running', status calculation_status DEFAULT 'running',
error_message TEXT, error_message TEXT,
additional_info JSON, additional_info JSONB
INDEX idx_status_time (status, start_time)
); );
CREATE TABLE IF NOT EXISTS calculate_status ( CREATE TABLE IF NOT EXISTS calculate_status (
module_name ENUM( module_name module_name PRIMARY KEY,
'product_metrics', last_calculation_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
'time_aggregates',
'financial_metrics',
'vendor_metrics',
'category_metrics',
'brand_metrics',
'sales_forecasts',
'abc_classification'
) PRIMARY KEY,
last_calculation_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_last_calc (last_calculation_timestamp)
); );
CREATE TABLE IF NOT EXISTS sync_status ( CREATE TABLE IF NOT EXISTS sync_status (
table_name VARCHAR(50) PRIMARY KEY, table_name VARCHAR(50) PRIMARY KEY,
last_sync_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_sync_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_sync_id BIGINT, last_sync_id BIGINT
INDEX idx_last_sync (last_sync_timestamp)
); );
CREATE TABLE IF NOT EXISTS import_history ( CREATE TABLE IF NOT EXISTS import_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
table_name VARCHAR(50) NOT NULL, table_name VARCHAR(50) NOT NULL,
start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP NULL, end_time TIMESTAMP NULL,
duration_seconds INT, duration_seconds INTEGER,
duration_minutes DECIMAL(10,2) GENERATED ALWAYS AS (duration_seconds / 60.0) STORED, duration_minutes DECIMAL(10,2) GENERATED ALWAYS AS (duration_seconds::decimal / 60.0) STORED,
records_added INT DEFAULT 0, records_added INTEGER DEFAULT 0,
records_updated INT DEFAULT 0, records_updated INTEGER DEFAULT 0,
is_incremental BOOLEAN DEFAULT FALSE, is_incremental BOOLEAN DEFAULT FALSE,
status ENUM('running', 'completed', 'failed', 'cancelled') DEFAULT 'running', status calculation_status DEFAULT 'running',
error_message TEXT, error_message TEXT,
additional_info JSON, additional_info JSONB
INDEX idx_table_time (table_name, start_time), );
INDEX idx_status (status)
); -- Create all indexes after tables are fully created
CREATE INDEX IF NOT EXISTS idx_last_calc ON calculate_status(last_calculation_timestamp);
CREATE INDEX IF NOT EXISTS idx_last_sync ON sync_status(last_sync_timestamp);
CREATE INDEX IF NOT EXISTS idx_table_time ON import_history(table_name, start_time);

View File

@@ -1,8 +1,8 @@
-- Disable foreign key checks -- Disable foreign key checks
SET FOREIGN_KEY_CHECKS = 0; SET session_replication_role = 'replica';
-- Temporary tables for batch metrics processing -- Temporary tables for batch metrics processing
CREATE TABLE IF NOT EXISTS temp_sales_metrics ( CREATE TABLE temp_sales_metrics (
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
daily_sales_avg DECIMAL(10,3), daily_sales_avg DECIMAL(10,3),
weekly_sales_avg DECIMAL(10,3), weekly_sales_avg DECIMAL(10,3),
@@ -14,9 +14,9 @@ CREATE TABLE IF NOT EXISTS temp_sales_metrics (
PRIMARY KEY (pid) PRIMARY KEY (pid)
); );
CREATE TABLE IF NOT EXISTS temp_purchase_metrics ( CREATE TABLE temp_purchase_metrics (
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
avg_lead_time_days INT, avg_lead_time_days INTEGER,
last_purchase_date DATE, last_purchase_date DATE,
first_received_date DATE, first_received_date DATE,
last_received_date DATE, last_received_date DATE,
@@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS temp_purchase_metrics (
); );
-- New table for product metrics -- New table for product metrics
CREATE TABLE IF NOT EXISTS product_metrics ( CREATE TABLE product_metrics (
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Sales velocity metrics -- Sales velocity metrics
@@ -32,16 +32,16 @@ CREATE TABLE IF NOT EXISTS product_metrics (
weekly_sales_avg DECIMAL(10,3), weekly_sales_avg DECIMAL(10,3),
monthly_sales_avg DECIMAL(10,3), monthly_sales_avg DECIMAL(10,3),
avg_quantity_per_order DECIMAL(10,3), avg_quantity_per_order DECIMAL(10,3),
number_of_orders INT, number_of_orders INTEGER,
first_sale_date DATE, first_sale_date DATE,
last_sale_date DATE, last_sale_date DATE,
-- Stock metrics -- Stock metrics
days_of_inventory INT, days_of_inventory INTEGER,
weeks_of_inventory INT, weeks_of_inventory INTEGER,
reorder_point INT, reorder_point INTEGER,
safety_stock INT, safety_stock INTEGER,
reorder_qty INT DEFAULT 0, reorder_qty INTEGER DEFAULT 0,
overstocked_amt INT DEFAULT 0, overstocked_amt INTEGER DEFAULT 0,
-- Financial metrics -- Financial metrics
avg_margin_percent DECIMAL(10,3), avg_margin_percent DECIMAL(10,3),
total_revenue DECIMAL(10,3), total_revenue DECIMAL(10,3),
@@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS product_metrics (
gross_profit DECIMAL(10,3), gross_profit DECIMAL(10,3),
gmroi DECIMAL(10,3), gmroi DECIMAL(10,3),
-- Purchase metrics -- Purchase metrics
avg_lead_time_days INT, avg_lead_time_days INTEGER,
last_purchase_date DATE, last_purchase_date DATE,
first_received_date DATE, first_received_date DATE,
last_received_date DATE, last_received_date DATE,
@@ -60,48 +60,50 @@ CREATE TABLE IF NOT EXISTS product_metrics (
-- Turnover metrics -- Turnover metrics
turnover_rate DECIMAL(12,3), turnover_rate DECIMAL(12,3),
-- Lead time metrics -- Lead time metrics
current_lead_time INT, current_lead_time INTEGER,
target_lead_time INT, target_lead_time INTEGER,
lead_time_status VARCHAR(20), lead_time_status VARCHAR(20),
-- Forecast metrics -- Forecast metrics
forecast_accuracy DECIMAL(5,2) DEFAULT NULL, forecast_accuracy DECIMAL(5,2) DEFAULT NULL,
forecast_bias DECIMAL(5,2) DEFAULT NULL, forecast_bias DECIMAL(5,2) DEFAULT NULL,
last_forecast_date DATE DEFAULT NULL, last_forecast_date DATE DEFAULT NULL,
PRIMARY KEY (pid), PRIMARY KEY (pid),
FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE, FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE
INDEX idx_metrics_revenue (total_revenue),
INDEX idx_metrics_stock_status (stock_status),
INDEX idx_metrics_lead_time (lead_time_status),
INDEX idx_metrics_turnover (turnover_rate),
INDEX idx_metrics_last_calculated (last_calculated_at),
INDEX idx_metrics_abc (abc_class),
INDEX idx_metrics_sales (daily_sales_avg, weekly_sales_avg, monthly_sales_avg),
INDEX idx_metrics_forecast (forecast_accuracy, forecast_bias)
); );
CREATE INDEX idx_metrics_revenue ON product_metrics(total_revenue);
CREATE INDEX idx_metrics_stock_status ON product_metrics(stock_status);
CREATE INDEX idx_metrics_lead_time ON product_metrics(lead_time_status);
CREATE INDEX idx_metrics_turnover ON product_metrics(turnover_rate);
CREATE INDEX idx_metrics_last_calculated ON product_metrics(last_calculated_at);
CREATE INDEX idx_metrics_abc ON product_metrics(abc_class);
CREATE INDEX idx_metrics_sales ON product_metrics(daily_sales_avg, weekly_sales_avg, monthly_sales_avg);
CREATE INDEX idx_metrics_forecast ON product_metrics(forecast_accuracy, forecast_bias);
-- New table for time-based aggregates -- New table for time-based aggregates
CREATE TABLE IF NOT EXISTS product_time_aggregates ( CREATE TABLE product_time_aggregates (
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
year INT NOT NULL, year INTEGER NOT NULL,
month INT NOT NULL, month INTEGER NOT NULL,
-- Sales metrics -- Sales metrics
total_quantity_sold INT DEFAULT 0, total_quantity_sold INTEGER DEFAULT 0,
total_revenue DECIMAL(10,3) DEFAULT 0, total_revenue DECIMAL(10,3) DEFAULT 0,
total_cost DECIMAL(10,3) DEFAULT 0, total_cost DECIMAL(10,3) DEFAULT 0,
order_count INT DEFAULT 0, order_count INTEGER DEFAULT 0,
-- Stock changes -- Stock changes
stock_received INT DEFAULT 0, stock_received INTEGER DEFAULT 0,
stock_ordered INT DEFAULT 0, stock_ordered INTEGER DEFAULT 0,
-- Calculated fields -- Calculated fields
avg_price DECIMAL(10,3), avg_price DECIMAL(10,3),
profit_margin DECIMAL(10,3), profit_margin DECIMAL(10,3),
inventory_value DECIMAL(10,3), inventory_value DECIMAL(10,3),
gmroi DECIMAL(10,3), gmroi DECIMAL(10,3),
PRIMARY KEY (pid, year, month), PRIMARY KEY (pid, year, month),
FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE, FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE
INDEX idx_date (year, month)
); );
CREATE INDEX idx_date ON product_time_aggregates(year, month);
-- Create vendor_details table -- Create vendor_details table
CREATE TABLE vendor_details ( CREATE TABLE vendor_details (
vendor VARCHAR(100) PRIMARY KEY, vendor VARCHAR(100) PRIMARY KEY,
@@ -110,45 +112,47 @@ CREATE TABLE vendor_details (
phone VARCHAR(50), phone VARCHAR(50),
status VARCHAR(20) DEFAULT 'active', status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
INDEX idx_status (status) );
) ENGINE=InnoDB;
CREATE INDEX idx_vendor_details_status ON vendor_details(status);
-- New table for vendor metrics -- New table for vendor metrics
CREATE TABLE IF NOT EXISTS vendor_metrics ( CREATE TABLE vendor_metrics (
vendor VARCHAR(100) NOT NULL, vendor VARCHAR(100) NOT NULL,
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Performance metrics -- Performance metrics
avg_lead_time_days DECIMAL(10,3), avg_lead_time_days DECIMAL(10,3),
on_time_delivery_rate DECIMAL(5,2), on_time_delivery_rate DECIMAL(5,2),
order_fill_rate DECIMAL(5,2), order_fill_rate DECIMAL(5,2),
total_orders INT DEFAULT 0, total_orders INTEGER DEFAULT 0,
total_late_orders INT DEFAULT 0, total_late_orders INTEGER DEFAULT 0,
total_purchase_value DECIMAL(10,3) DEFAULT 0, total_purchase_value DECIMAL(10,3) DEFAULT 0,
avg_order_value DECIMAL(10,3), avg_order_value DECIMAL(10,3),
-- Product metrics -- Product metrics
active_products INT DEFAULT 0, active_products INTEGER DEFAULT 0,
total_products INT DEFAULT 0, total_products INTEGER DEFAULT 0,
-- Financial metrics -- Financial metrics
total_revenue DECIMAL(10,3) DEFAULT 0, total_revenue DECIMAL(10,3) DEFAULT 0,
avg_margin_percent DECIMAL(5,2), avg_margin_percent DECIMAL(5,2),
-- Status -- Status
status VARCHAR(20) DEFAULT 'active', status VARCHAR(20) DEFAULT 'active',
PRIMARY KEY (vendor), PRIMARY KEY (vendor),
FOREIGN KEY (vendor) REFERENCES vendor_details(vendor) ON DELETE CASCADE, FOREIGN KEY (vendor) REFERENCES vendor_details(vendor) ON DELETE CASCADE
INDEX idx_vendor_performance (on_time_delivery_rate),
INDEX idx_vendor_status (status),
INDEX idx_metrics_last_calculated (last_calculated_at),
INDEX idx_vendor_metrics_orders (total_orders, total_late_orders)
); );
CREATE INDEX idx_vendor_performance ON vendor_metrics(on_time_delivery_rate);
CREATE INDEX idx_vendor_status ON vendor_metrics(status);
CREATE INDEX idx_vendor_metrics_last_calculated ON vendor_metrics(last_calculated_at);
CREATE INDEX idx_vendor_metrics_orders ON vendor_metrics(total_orders, total_late_orders);
-- New table for category metrics -- New table for category metrics
CREATE TABLE IF NOT EXISTS category_metrics ( CREATE TABLE category_metrics (
category_id BIGINT NOT NULL, category_id BIGINT NOT NULL,
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Product metrics -- Product metrics
product_count INT DEFAULT 0, product_count INTEGER DEFAULT 0,
active_products INT DEFAULT 0, active_products INTEGER DEFAULT 0,
-- Financial metrics -- Financial metrics
total_value DECIMAL(15,3) DEFAULT 0, total_value DECIMAL(15,3) DEFAULT 0,
avg_margin DECIMAL(5,2), avg_margin DECIMAL(5,2),
@@ -157,255 +161,215 @@ CREATE TABLE IF NOT EXISTS category_metrics (
-- Status -- Status
status VARCHAR(20) DEFAULT 'active', status VARCHAR(20) DEFAULT 'active',
PRIMARY KEY (category_id), PRIMARY KEY (category_id),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE
INDEX idx_category_status (status),
INDEX idx_category_growth (growth_rate),
INDEX idx_metrics_last_calculated (last_calculated_at),
INDEX idx_category_metrics_products (product_count, active_products)
); );
CREATE INDEX idx_category_status ON category_metrics(status);
CREATE INDEX idx_category_growth ON category_metrics(growth_rate);
CREATE INDEX idx_metrics_last_calculated_cat ON category_metrics(last_calculated_at);
CREATE INDEX idx_category_metrics_products ON category_metrics(product_count, active_products);
-- New table for vendor time-based metrics -- New table for vendor time-based metrics
CREATE TABLE IF NOT EXISTS vendor_time_metrics ( CREATE TABLE vendor_time_metrics (
vendor VARCHAR(100) NOT NULL, vendor VARCHAR(100) NOT NULL,
year INT NOT NULL, year INTEGER NOT NULL,
month INT NOT NULL, month INTEGER NOT NULL,
-- Order metrics -- Order metrics
total_orders INT DEFAULT 0, total_orders INTEGER DEFAULT 0,
late_orders INT DEFAULT 0, late_orders INTEGER DEFAULT 0,
avg_lead_time_days DECIMAL(10,3), avg_lead_time_days DECIMAL(10,3),
-- Financial metrics -- Financial metrics
total_purchase_value DECIMAL(10,3) DEFAULT 0, total_purchase_value DECIMAL(10,3) DEFAULT 0,
total_revenue DECIMAL(10,3) DEFAULT 0, total_revenue DECIMAL(10,3) DEFAULT 0,
avg_margin_percent DECIMAL(5,2), avg_margin_percent DECIMAL(5,2),
PRIMARY KEY (vendor, year, month), PRIMARY KEY (vendor, year, month),
FOREIGN KEY (vendor) REFERENCES vendor_details(vendor) ON DELETE CASCADE, FOREIGN KEY (vendor) REFERENCES vendor_details(vendor) ON DELETE CASCADE
INDEX idx_vendor_date (year, month)
); );
CREATE INDEX idx_vendor_date ON vendor_time_metrics(year, month);
-- New table for category time-based metrics -- New table for category time-based metrics
CREATE TABLE IF NOT EXISTS category_time_metrics ( CREATE TABLE category_time_metrics (
category_id BIGINT NOT NULL, category_id BIGINT NOT NULL,
year INT NOT NULL, year INTEGER NOT NULL,
month INT NOT NULL, month INTEGER NOT NULL,
-- Product metrics -- Product metrics
product_count INT DEFAULT 0, product_count INTEGER DEFAULT 0,
active_products INT DEFAULT 0, active_products INTEGER DEFAULT 0,
-- Financial metrics -- Financial metrics
total_value DECIMAL(15,3) DEFAULT 0, total_value DECIMAL(15,3) DEFAULT 0,
total_revenue DECIMAL(15,3) DEFAULT 0, total_revenue DECIMAL(15,3) DEFAULT 0,
avg_margin DECIMAL(5,2), avg_margin DECIMAL(5,2),
turnover_rate DECIMAL(12,3), turnover_rate DECIMAL(12,3),
PRIMARY KEY (category_id, year, month), PRIMARY KEY (category_id, year, month),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE
INDEX idx_category_date (year, month)
); );
CREATE INDEX idx_category_date ON category_time_metrics(year, month);
-- New table for category-based sales metrics -- New table for category-based sales metrics
CREATE TABLE IF NOT EXISTS category_sales_metrics ( CREATE TABLE category_sales_metrics (
category_id BIGINT NOT NULL, category_id BIGINT NOT NULL,
brand VARCHAR(100) NOT NULL, brand VARCHAR(100) NOT NULL,
period_start DATE NOT NULL, period_start DATE NOT NULL,
period_end DATE NOT NULL, period_end DATE NOT NULL,
avg_daily_sales DECIMAL(10,3) DEFAULT 0, avg_daily_sales DECIMAL(10,3) DEFAULT 0,
total_sold INT DEFAULT 0, total_sold INTEGER DEFAULT 0,
num_products INT DEFAULT 0, num_products INTEGER DEFAULT 0,
avg_price DECIMAL(10,3) DEFAULT 0, avg_price DECIMAL(10,3) DEFAULT 0,
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (category_id, brand, period_start, period_end), PRIMARY KEY (category_id, brand, period_start, period_end),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE
INDEX idx_category_brand (category_id, brand),
INDEX idx_period (period_start, period_end)
); );
CREATE INDEX idx_category_brand ON category_sales_metrics(category_id, brand);
CREATE INDEX idx_period ON category_sales_metrics(period_start, period_end);
-- New table for brand metrics -- New table for brand metrics
CREATE TABLE IF NOT EXISTS brand_metrics ( CREATE TABLE brand_metrics (
brand VARCHAR(100) NOT NULL, brand VARCHAR(100) NOT NULL,
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Product metrics -- Product metrics
product_count INT DEFAULT 0, product_count INTEGER DEFAULT 0,
active_products INT DEFAULT 0, active_products INTEGER DEFAULT 0,
-- Stock metrics -- Stock metrics
total_stock_units INT DEFAULT 0, total_stock_units INTEGER DEFAULT 0,
total_stock_cost DECIMAL(15,2) DEFAULT 0, total_stock_cost DECIMAL(15,2) DEFAULT 0,
total_stock_retail DECIMAL(15,2) DEFAULT 0, total_stock_retail DECIMAL(15,2) DEFAULT 0,
-- Sales metrics -- Sales metrics
total_revenue DECIMAL(15,2) DEFAULT 0, total_revenue DECIMAL(15,2) DEFAULT 0,
avg_margin DECIMAL(5,2) DEFAULT 0, avg_margin DECIMAL(5,2) DEFAULT 0,
growth_rate DECIMAL(5,2) DEFAULT 0, growth_rate DECIMAL(5,2) DEFAULT 0,
PRIMARY KEY (brand), PRIMARY KEY (brand)
INDEX idx_brand_metrics_last_calculated (last_calculated_at),
INDEX idx_brand_metrics_revenue (total_revenue),
INDEX idx_brand_metrics_growth (growth_rate)
); );
CREATE INDEX idx_brand_metrics_last_calculated ON brand_metrics(last_calculated_at);
CREATE INDEX idx_brand_metrics_revenue ON brand_metrics(total_revenue);
CREATE INDEX idx_brand_metrics_growth ON brand_metrics(growth_rate);
-- New table for brand time-based metrics -- New table for brand time-based metrics
CREATE TABLE IF NOT EXISTS brand_time_metrics ( CREATE TABLE brand_time_metrics (
brand VARCHAR(100) NOT NULL, brand VARCHAR(100) NOT NULL,
year INT NOT NULL, year INTEGER NOT NULL,
month INT NOT NULL, month INTEGER NOT NULL,
-- Product metrics -- Product metrics
product_count INT DEFAULT 0, product_count INTEGER DEFAULT 0,
active_products INT DEFAULT 0, active_products INTEGER DEFAULT 0,
-- Stock metrics -- Stock metrics
total_stock_units INT DEFAULT 0, total_stock_units INTEGER DEFAULT 0,
total_stock_cost DECIMAL(15,2) DEFAULT 0, total_stock_cost DECIMAL(15,2) DEFAULT 0,
total_stock_retail DECIMAL(15,2) DEFAULT 0, total_stock_retail DECIMAL(15,2) DEFAULT 0,
-- Sales metrics -- Sales metrics
total_revenue DECIMAL(15,2) DEFAULT 0, total_revenue DECIMAL(15,2) DEFAULT 0,
avg_margin DECIMAL(5,2) DEFAULT 0, avg_margin DECIMAL(5,2) DEFAULT 0,
PRIMARY KEY (brand, year, month), growth_rate DECIMAL(5,2) DEFAULT 0,
INDEX idx_brand_date (year, month) PRIMARY KEY (brand, year, month)
); );
CREATE INDEX idx_brand_time_date ON brand_time_metrics(year, month);
-- New table for sales forecasts -- New table for sales forecasts
CREATE TABLE IF NOT EXISTS sales_forecasts ( CREATE TABLE sales_forecasts (
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
forecast_date DATE NOT NULL, forecast_date DATE NOT NULL,
forecast_units DECIMAL(10,2) DEFAULT 0, forecast_quantity INTEGER,
forecast_revenue DECIMAL(10,2) DEFAULT 0, confidence_level DECIMAL(5,2),
confidence_level DECIMAL(5,2) DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (pid, forecast_date), PRIMARY KEY (pid, forecast_date),
FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE, FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE
INDEX idx_forecast_date (forecast_date),
INDEX idx_forecast_last_calculated (last_calculated_at)
); );
CREATE INDEX idx_forecast_date ON sales_forecasts(forecast_date);
-- New table for category forecasts -- New table for category forecasts
CREATE TABLE IF NOT EXISTS category_forecasts ( CREATE TABLE category_forecasts (
category_id BIGINT NOT NULL, category_id BIGINT NOT NULL,
forecast_date DATE NOT NULL, forecast_date DATE NOT NULL,
forecast_units DECIMAL(10,2) DEFAULT 0, forecast_revenue DECIMAL(15,2),
forecast_revenue DECIMAL(10,2) DEFAULT 0, forecast_units INTEGER,
confidence_level DECIMAL(5,2) DEFAULT 0, confidence_level DECIMAL(5,2),
last_calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (category_id, forecast_date), PRIMARY KEY (category_id, forecast_date),
FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(cat_id) ON DELETE CASCADE
INDEX idx_category_forecast_date (forecast_date),
INDEX idx_category_forecast_last_calculated (last_calculated_at)
); );
-- Create view for inventory health CREATE INDEX idx_cat_forecast_date ON category_forecasts(forecast_date);
-- Create views for common calculations
CREATE OR REPLACE VIEW inventory_health AS CREATE OR REPLACE VIEW inventory_health AS
WITH product_thresholds AS ( WITH stock_levels AS (
SELECT SELECT
p.pid, p.pid,
COALESCE( p.title,
-- Try category+vendor specific p.SKU,
(SELECT critical_days FROM stock_thresholds st p.stock_quantity,
JOIN product_categories pc ON st.category_id = pc.cat_id p.preorder_count,
WHERE pc.pid = p.pid pm.daily_sales_avg,
AND st.vendor = p.vendor LIMIT 1), pm.weekly_sales_avg,
-- Try category specific pm.monthly_sales_avg,
(SELECT critical_days FROM stock_thresholds st pm.reorder_point,
JOIN product_categories pc ON st.category_id = pc.cat_id pm.safety_stock,
WHERE pc.pid = p.pid pm.days_of_inventory,
AND st.vendor IS NULL LIMIT 1), pm.weeks_of_inventory,
-- Try vendor specific pm.stock_status,
(SELECT critical_days FROM stock_thresholds st pm.abc_class,
WHERE st.category_id IS NULL pm.turnover_rate,
AND st.vendor = p.vendor LIMIT 1), pm.avg_lead_time_days,
-- Fall back to default pm.current_lead_time,
(SELECT critical_days FROM stock_thresholds st pm.target_lead_time,
WHERE st.category_id IS NULL pm.lead_time_status,
AND st.vendor IS NULL LIMIT 1), p.cost_price,
7 p.price,
) as critical_days, pm.inventory_value,
COALESCE( pm.gmroi
-- Try category+vendor specific
(SELECT reorder_days FROM stock_thresholds st
JOIN product_categories pc ON st.category_id = pc.cat_id
WHERE pc.pid = p.pid
AND st.vendor = p.vendor LIMIT 1),
-- Try category specific
(SELECT reorder_days FROM stock_thresholds st
JOIN product_categories pc ON st.category_id = pc.cat_id
WHERE pc.pid = p.pid
AND st.vendor IS NULL LIMIT 1),
-- Try vendor specific
(SELECT reorder_days FROM stock_thresholds st
WHERE st.category_id IS NULL
AND st.vendor = p.vendor LIMIT 1),
-- Fall back to default
(SELECT reorder_days FROM stock_thresholds st
WHERE st.category_id IS NULL
AND st.vendor IS NULL LIMIT 1),
14
) as reorder_days,
COALESCE(
-- Try category+vendor specific
(SELECT overstock_days FROM stock_thresholds st
JOIN product_categories pc ON st.category_id = pc.cat_id
WHERE pc.pid = p.pid
AND st.vendor = p.vendor LIMIT 1),
-- Try category specific
(SELECT overstock_days FROM stock_thresholds st
JOIN product_categories pc ON st.category_id = pc.cat_id
WHERE pc.pid = p.pid
AND st.vendor IS NULL LIMIT 1),
-- Try vendor specific
(SELECT overstock_days FROM stock_thresholds st
WHERE st.category_id IS NULL
AND st.vendor = p.vendor LIMIT 1),
-- Fall back to default
(SELECT overstock_days FROM stock_thresholds st
WHERE st.category_id IS NULL
AND st.vendor IS NULL LIMIT 1),
90
) as overstock_days
FROM products p FROM products p
LEFT JOIN product_metrics pm ON p.pid = pm.pid
WHERE p.managing_stock = true AND p.visible = true
) )
SELECT SELECT
p.pid, *,
p.SKU,
p.title,
p.stock_quantity,
COALESCE(pm.daily_sales_avg, 0) as daily_sales_avg,
COALESCE(pm.days_of_inventory, 0) as days_of_inventory,
COALESCE(pm.reorder_point, 0) as reorder_point,
COALESCE(pm.safety_stock, 0) as safety_stock,
CASE CASE
WHEN pm.daily_sales_avg = 0 THEN 'New' WHEN stock_quantity <= safety_stock THEN 'Critical'
WHEN p.stock_quantity <= CEIL(pm.daily_sales_avg * pt.critical_days) THEN 'Critical' WHEN stock_quantity <= reorder_point THEN 'Low'
WHEN p.stock_quantity <= CEIL(pm.daily_sales_avg * pt.reorder_days) THEN 'Reorder' WHEN stock_quantity > (reorder_point * 3) THEN 'Excess'
WHEN p.stock_quantity > (pm.daily_sales_avg * pt.overstock_days) THEN 'Overstocked'
ELSE 'Healthy' ELSE 'Healthy'
END as stock_status END as inventory_status,
FROM CASE
products p WHEN lead_time_status = 'delayed' AND stock_status = 'low' THEN 'High'
LEFT JOIN WHEN lead_time_status = 'delayed' OR stock_status = 'low' THEN 'Medium'
product_metrics pm ON p.pid = pm.pid ELSE 'Low'
LEFT JOIN END as risk_level
product_thresholds pt ON p.pid = pt.pid FROM stock_levels;
WHERE
p.managing_stock = true;
-- Create view for category performance trends -- Create view for category performance trends
CREATE OR REPLACE VIEW category_performance_trends AS CREATE OR REPLACE VIEW category_performance_trends AS
WITH monthly_trends AS (
SELECT
c.cat_id,
c.name as category_name,
ctm.year,
ctm.month,
ctm.product_count,
ctm.active_products,
ctm.total_value,
ctm.total_revenue,
ctm.avg_margin,
ctm.turnover_rate,
LAG(ctm.total_revenue) OVER (PARTITION BY c.cat_id ORDER BY ctm.year, ctm.month) as prev_month_revenue,
LAG(ctm.turnover_rate) OVER (PARTITION BY c.cat_id ORDER BY ctm.year, ctm.month) as prev_month_turnover
FROM categories c
JOIN category_time_metrics ctm ON c.cat_id = ctm.category_id
)
SELECT SELECT
c.cat_id as category_id, *,
c.name,
c.description,
p.name as parent_name,
c.status,
cm.product_count,
cm.active_products,
cm.total_value,
cm.avg_margin,
cm.turnover_rate,
cm.growth_rate,
CASE CASE
WHEN cm.growth_rate >= 20 THEN 'High Growth' WHEN prev_month_revenue IS NULL THEN 0
WHEN cm.growth_rate >= 5 THEN 'Growing' ELSE ((total_revenue - prev_month_revenue) / prev_month_revenue) * 100
WHEN cm.growth_rate >= -5 THEN 'Stable' END as revenue_growth_percent,
ELSE 'Declining' CASE
END as performance_rating WHEN prev_month_turnover IS NULL THEN 0
FROM ELSE ((turnover_rate - prev_month_turnover) / prev_month_turnover) * 100
categories c END as turnover_growth_percent
LEFT JOIN FROM monthly_trends;
categories p ON c.parent_id = p.cat_id
LEFT JOIN
category_metrics cm ON c.cat_id = cm.category_id;
-- Re-enable foreign key checks SET session_replication_role = 'origin';
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,6 +1,5 @@
-- Enable strict error reporting -- Enable strict error reporting
SET sql_mode = 'STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ZERO_DATE,NO_ZERO_IN_DATE,NO_ENGINE_SUBSTITUTION'; SET session_replication_role = 'replica'; -- Disable foreign key checks temporarily
SET FOREIGN_KEY_CHECKS = 0;
-- Create tables -- Create tables
CREATE TABLE products ( CREATE TABLE products (
@@ -8,11 +7,11 @@ CREATE TABLE products (
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
description TEXT, description TEXT,
SKU VARCHAR(50) NOT NULL, SKU VARCHAR(50) NOT NULL,
created_at TIMESTAMP NULL, created_at TIMESTAMP,
first_received TIMESTAMP NULL, first_received TIMESTAMP,
stock_quantity INT DEFAULT 0, stock_quantity INTEGER DEFAULT 0,
preorder_count INT DEFAULT 0, preorder_count INTEGER DEFAULT 0,
notions_inv_count INT DEFAULT 0, notions_inv_count INTEGER DEFAULT 0,
price DECIMAL(10, 3) NOT NULL, price DECIMAL(10, 3) NOT NULL,
regular_price DECIMAL(10, 3) NOT NULL, regular_price DECIMAL(10, 3) NOT NULL,
cost_price DECIMAL(10, 3), cost_price DECIMAL(10, 3),
@@ -37,47 +36,52 @@ CREATE TABLE products (
artist VARCHAR(100), artist VARCHAR(100),
options TEXT, options TEXT,
tags TEXT, tags TEXT,
moq INT DEFAULT 1, moq INTEGER DEFAULT 1,
uom INT DEFAULT 1, uom INTEGER DEFAULT 1,
rating DECIMAL(10,2) DEFAULT 0.00, rating DECIMAL(10,2) DEFAULT 0.00,
reviews INT UNSIGNED DEFAULT 0, reviews INTEGER DEFAULT 0,
weight DECIMAL(10,3), weight DECIMAL(10,3),
length DECIMAL(10,3), length DECIMAL(10,3),
width DECIMAL(10,3), width DECIMAL(10,3),
height DECIMAL(10,3), height DECIMAL(10,3),
country_of_origin VARCHAR(5), country_of_origin VARCHAR(5),
location VARCHAR(50), location VARCHAR(50),
total_sold INT UNSIGNED DEFAULT 0, total_sold INTEGER DEFAULT 0,
baskets INT UNSIGNED DEFAULT 0, baskets INTEGER DEFAULT 0,
notifies INT UNSIGNED DEFAULT 0, notifies INTEGER DEFAULT 0,
date_last_sold DATE, date_last_sold DATE,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (pid), PRIMARY KEY (pid)
INDEX idx_sku (SKU), );
INDEX idx_vendor (vendor),
INDEX idx_brand (brand), -- Create indexes for products table
INDEX idx_location (location), CREATE INDEX idx_products_sku ON products(SKU);
INDEX idx_total_sold (total_sold), CREATE INDEX idx_products_vendor ON products(vendor);
INDEX idx_date_last_sold (date_last_sold), CREATE INDEX idx_products_brand ON products(brand);
INDEX idx_updated (updated) CREATE INDEX idx_products_location ON products(location);
) ENGINE=InnoDB; CREATE INDEX idx_products_total_sold ON products(total_sold);
CREATE INDEX idx_products_date_last_sold ON products(date_last_sold);
CREATE INDEX idx_products_updated ON products(updated);
-- Create categories table with hierarchy support -- Create categories table with hierarchy support
CREATE TABLE categories ( CREATE TABLE categories (
cat_id BIGINT PRIMARY KEY, cat_id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
type SMALLINT NOT NULL COMMENT '10=section, 11=category, 12=subcategory, 13=subsubcategory, 1=company, 2=line, 3=subline, 40=artist', type SMALLINT NOT NULL,
parent_id BIGINT, parent_id BIGINT,
description TEXT, description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'active', status VARCHAR(20) DEFAULT 'active',
FOREIGN KEY (parent_id) REFERENCES categories(cat_id), FOREIGN KEY (parent_id) REFERENCES categories(cat_id)
INDEX idx_parent (parent_id), );
INDEX idx_type (type),
INDEX idx_status (status), COMMENT ON COLUMN categories.type IS '10=section, 11=category, 12=subcategory, 13=subsubcategory, 1=company, 2=line, 3=subline, 40=artist';
INDEX idx_name_type (name, type)
) ENGINE=InnoDB; CREATE INDEX idx_categories_parent ON categories(parent_id);
CREATE INDEX idx_categories_type ON categories(type);
CREATE INDEX idx_categories_status ON categories(status);
CREATE INDEX idx_categories_name_type ON categories(name, type);
-- Create product_categories junction table -- Create product_categories junction table
CREATE TABLE product_categories ( CREATE TABLE product_categories (
@@ -85,78 +89,86 @@ CREATE TABLE product_categories (
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
PRIMARY KEY (pid, cat_id), PRIMARY KEY (pid, cat_id),
FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE, FOREIGN KEY (pid) REFERENCES products(pid) ON DELETE CASCADE,
FOREIGN KEY (cat_id) REFERENCES categories(cat_id) ON DELETE CASCADE, FOREIGN KEY (cat_id) REFERENCES categories(cat_id) ON DELETE CASCADE
INDEX idx_category (cat_id), );
INDEX idx_product (pid)
) ENGINE=InnoDB; CREATE INDEX idx_product_categories_category ON product_categories(cat_id);
CREATE INDEX idx_product_categories_product ON product_categories(pid);
-- Create orders table with its indexes -- Create orders table with its indexes
CREATE TABLE IF NOT EXISTS orders ( CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT, id BIGSERIAL PRIMARY KEY,
order_number VARCHAR(50) NOT NULL, order_number VARCHAR(50) NOT NULL,
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
SKU VARCHAR(50) NOT NULL, SKU VARCHAR(50) NOT NULL,
date DATE NOT NULL, date DATE NOT NULL,
price DECIMAL(10,3) NOT NULL, price DECIMAL(10,3) NOT NULL,
quantity INT NOT NULL, quantity INTEGER NOT NULL,
discount DECIMAL(10,3) DEFAULT 0.000, discount DECIMAL(10,3) DEFAULT 0.000,
tax DECIMAL(10,3) DEFAULT 0.000, tax DECIMAL(10,3) DEFAULT 0.000,
tax_included TINYINT(1) DEFAULT 0, tax_included BOOLEAN DEFAULT false,
shipping DECIMAL(10,3) DEFAULT 0.000, shipping DECIMAL(10,3) DEFAULT 0.000,
costeach DECIMAL(10,3) DEFAULT 0.000, costeach DECIMAL(10,3) DEFAULT 0.000,
customer VARCHAR(50) NOT NULL, customer VARCHAR(50) NOT NULL,
customer_name VARCHAR(100), customer_name VARCHAR(100),
status VARCHAR(20) DEFAULT 'pending', status VARCHAR(20) DEFAULT 'pending',
canceled TINYINT(1) DEFAULT 0, canceled BOOLEAN DEFAULT false,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), UNIQUE (order_number, pid)
UNIQUE KEY unique_order_line (order_number, pid), );
KEY order_number (order_number),
KEY pid (pid), CREATE INDEX idx_orders_number ON orders(order_number);
KEY customer (customer), CREATE INDEX idx_orders_pid ON orders(pid);
KEY date (date), CREATE INDEX idx_orders_customer ON orders(customer);
KEY status (status), CREATE INDEX idx_orders_date ON orders(date);
INDEX idx_orders_metrics (pid, date, canceled), CREATE INDEX idx_orders_status ON orders(status);
INDEX idx_updated (updated) CREATE INDEX idx_orders_metrics ON orders(pid, date, canceled);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE INDEX idx_orders_updated ON orders(updated);
-- Create purchase_orders table with its indexes -- Create purchase_orders table with its indexes
CREATE TABLE purchase_orders ( CREATE TABLE purchase_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
po_id VARCHAR(50) NOT NULL, po_id VARCHAR(50) NOT NULL,
vendor VARCHAR(100) NOT NULL, vendor VARCHAR(100) NOT NULL,
date DATE NOT NULL, date DATE NOT NULL,
expected_date DATE, expected_date DATE,
pid BIGINT NOT NULL, pid BIGINT NOT NULL,
sku VARCHAR(50) NOT NULL, sku VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL COMMENT 'Product name from products.description', name VARCHAR(100) NOT NULL,
cost_price DECIMAL(10, 3) NOT NULL, cost_price DECIMAL(10, 3) NOT NULL,
po_cost_price DECIMAL(10, 3) NOT NULL COMMENT 'Original cost from PO, before receiving adjustments', po_cost_price DECIMAL(10, 3) NOT NULL,
status TINYINT UNSIGNED DEFAULT 1 COMMENT '0=canceled,1=created,10=electronically_ready_send,11=ordered,12=preordered,13=electronically_sent,15=receiving_started,50=done', status SMALLINT DEFAULT 1,
receiving_status TINYINT UNSIGNED DEFAULT 1 COMMENT '0=canceled,1=created,30=partial_received,40=full_received,50=paid', receiving_status SMALLINT DEFAULT 1,
notes TEXT, notes TEXT,
long_note TEXT, long_note TEXT,
ordered INT NOT NULL, ordered INTEGER NOT NULL,
received INT DEFAULT 0, received INTEGER DEFAULT 0,
received_date DATE COMMENT 'Date of first receiving', received_date DATE,
last_received_date DATE COMMENT 'Date of most recent receiving', last_received_date DATE,
received_by VARCHAR(100) COMMENT 'Name of person who first received this PO line', received_by VARCHAR(100),
receiving_history JSON COMMENT 'Array of receiving records with qty, date, cost, receiving_id, and alt_po flag', receiving_history JSONB,
updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (pid) REFERENCES products(pid), FOREIGN KEY (pid) REFERENCES products(pid),
INDEX idx_po_id (po_id), UNIQUE (po_id, pid)
INDEX idx_vendor (vendor), );
INDEX idx_status (status),
INDEX idx_receiving_status (receiving_status),
INDEX idx_purchase_orders_metrics (pid, date, status, ordered, received),
INDEX idx_po_metrics (pid, date, receiving_status, received_date),
INDEX idx_po_product_date (pid, date),
INDEX idx_po_product_status (pid, status),
INDEX idx_updated (updated),
UNIQUE KEY unique_po_product (po_id, pid)
) ENGINE=InnoDB;
SET FOREIGN_KEY_CHECKS = 1; COMMENT ON COLUMN purchase_orders.name IS 'Product name from products.description';
COMMENT ON COLUMN purchase_orders.po_cost_price IS 'Original cost from PO, before receiving adjustments';
COMMENT ON COLUMN purchase_orders.status IS '0=canceled,1=created,10=electronically_ready_send,11=ordered,12=preordered,13=electronically_sent,15=receiving_started,50=done';
COMMENT ON COLUMN purchase_orders.receiving_status IS '0=canceled,1=created,30=partial_received,40=full_received,50=paid';
COMMENT ON COLUMN purchase_orders.receiving_history IS 'Array of receiving records with qty, date, cost, receiving_id, and alt_po flag';
CREATE INDEX idx_po_id ON purchase_orders(po_id);
CREATE INDEX idx_po_vendor ON purchase_orders(vendor);
CREATE INDEX idx_po_status ON purchase_orders(status);
CREATE INDEX idx_po_receiving_status ON purchase_orders(receiving_status);
CREATE INDEX idx_po_metrics ON purchase_orders(pid, date, status, ordered, received);
CREATE INDEX idx_po_metrics_receiving ON purchase_orders(pid, date, receiving_status, received_date);
CREATE INDEX idx_po_product_date ON purchase_orders(pid, date);
CREATE INDEX idx_po_product_status ON purchase_orders(pid, status);
CREATE INDEX idx_po_updated ON purchase_orders(updated);
SET session_replication_role = 'origin'; -- Re-enable foreign key checks
-- Create views for common calculations -- Create views for common calculations
-- product_sales_trends view moved to metrics-schema.sql -- product_sales_trends view moved to metrics-schema.sql

View File

@@ -9,12 +9,14 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"pg": "^8.13.2",
"pm2": "^5.3.0", "pm2": "^5.3.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",
"uuid": "^9.0.1" "uuid": "^9.0.1"
@@ -23,6 +25,74 @@
"nodemon": "^3.0.2" "nodemon": "^3.0.2"
} }
}, },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/@pm2/agent": { "node_modules/@pm2/agent": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz",
@@ -276,6 +346,12 @@
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -322,6 +398,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -356,6 +441,40 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/are-we-there-yet/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -414,7 +533,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/basic-ftp": { "node_modules/basic-ftp": {
@@ -426,6 +544,20 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/bcrypt-pbkdf": { "node_modules/bcrypt-pbkdf": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -493,7 +625,6 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -619,6 +750,15 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/cli-tableau": { "node_modules/cli-tableau": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz",
@@ -648,6 +788,15 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "2.15.1", "version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
@@ -658,7 +807,6 @@
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/concat-stream": { "node_modules/concat-stream": {
@@ -676,6 +824,12 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -801,6 +955,12 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -829,6 +989,15 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -861,6 +1030,12 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -1132,6 +1307,36 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1155,6 +1360,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/generate-function": { "node_modules/generate-function": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -1250,6 +1476,27 @@
"integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==", "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -1295,6 +1542,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1414,6 +1667,17 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -1490,6 +1754,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1605,6 +1878,30 @@
"url": "https://github.com/sponsors/wellwelwel" "url": "https://github.com/sponsors/wellwelwel"
} }
}, },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1678,7 +1975,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@@ -1696,6 +1992,40 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -1857,6 +2187,32 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.9", "version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
@@ -1934,6 +2290,21 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -1943,6 +2314,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nssocket": { "node_modules/nssocket": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz", "resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz",
@@ -1995,6 +2379,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/pac-proxy-agent": { "node_modules/pac-proxy-agent": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz",
@@ -2065,6 +2458,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -2077,6 +2479,95 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pg": {
"version": "8.13.2",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.2.tgz",
"integrity": "sha512-L5QkPvTjVWWHbLaFjCkOSplpb2uCiRYbg0IJ2okCy5ClYfWlSgDDnvdR6dyw3EWAH2AfS4j8E61QFI7gLfTtlw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.1",
"pg-protocol": "^1.7.1",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz",
"integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz",
"integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -2320,6 +2811,45 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -2544,6 +3074,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/run-series": { "node_modules/run-series": {
"version": "1.1.9", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz",
@@ -2667,6 +3213,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -2850,6 +3402,15 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
@@ -2914,6 +3475,32 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2965,6 +3552,23 @@
"url": "https://www.buymeacoffee.com/systeminfo" "url": "https://www.buymeacoffee.com/systeminfo"
} }
}, },
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2996,6 +3600,12 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@@ -3132,6 +3742,37 @@
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": { "node_modules/ws": {
"version": "7.5.10", "version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",

View File

@@ -18,12 +18,14 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"pg": "^8.13.2",
"pm2": "^5.3.0", "pm2": "^5.3.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",
"uuid": "^9.0.1" "uuid": "^9.0.1"

View File

@@ -0,0 +1,79 @@
const { Client } = require('pg');
const bcrypt = require('bcrypt');
const path = require('path');
const readline = require('readline');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
const SALT_ROUNDS = 10;
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT || 5432
};
function prompt(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer);
});
});
}
async function addUser() {
try {
const username = await prompt('Enter username: ');
if (!username.trim()) {
console.error('Error: Username cannot be empty');
process.exit(1);
}
const password = await prompt('Enter password: ');
if (!password.trim()) {
console.error('Error: Password cannot be empty');
process.exit(1);
}
const client = new Client(dbConfig);
await client.connect();
// Check if user exists
const checkUser = await client.query(
'SELECT username FROM users WHERE username = $1',
[username]
);
if (checkUser.rows.length > 0) {
console.error('Error: Username already exists');
process.exit(1);
}
// Hash password
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
// Insert new user
await client.query(
'INSERT INTO users (username, password) VALUES ($1, $2)',
[username, hashedPassword]
);
console.log(`User '${username}' created successfully`);
await client.end();
} catch (error) {
console.error('Error creating user:', error.message);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
addUser();
}

View File

@@ -14,7 +14,15 @@ function outputProgress(data) {
function runScript(scriptPath) { function runScript(scriptPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn('node', [scriptPath], { const child = spawn('node', [scriptPath], {
stdio: ['inherit', 'pipe', 'pipe'] stdio: ['inherit', 'pipe', 'pipe'],
env: {
...process.env,
PGHOST: process.env.DB_HOST,
PGUSER: process.env.DB_USER,
PGPASSWORD: process.env.DB_PASSWORD,
PGDATABASE: process.env.DB_NAME,
PGPORT: process.env.DB_PORT || '5432'
}
}); });
let output = ''; let output = '';

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
const mysql = require('mysql2/promise'); const { Client } = require('pg');
const path = require('path'); const path = require('path');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const fs = require('fs'); const fs = require('fs');
@@ -10,7 +10,7 @@ const dbConfig = {
user: process.env.DB_USER, user: process.env.DB_USER,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
multipleStatements: true port: process.env.DB_PORT || 5432
}; };
// Helper function to output progress in JSON format // Helper function to output progress in JSON format
@@ -120,30 +120,26 @@ async function resetDatabase() {
} }
}); });
const connection = await mysql.createConnection(dbConfig); const client = new Client(dbConfig);
await client.connect();
try { try {
// Check MySQL privileges // Check PostgreSQL version and user
outputProgress({ outputProgress({
operation: 'Checking privileges', operation: 'Checking database',
message: 'Verifying MySQL user privileges...' message: 'Verifying PostgreSQL version and user privileges...'
}); });
const [grants] = await connection.query('SHOW GRANTS'); const versionResult = await client.query('SELECT version()');
outputProgress({ const userResult = await client.query('SELECT current_user, current_database()');
operation: 'User privileges',
message: {
grants: grants.map(g => Object.values(g)[0])
}
});
// Enable warnings as errors
await connection.query('SET SESSION sql_notes = 1');
// Log database config (without sensitive info)
outputProgress({ outputProgress({
operation: 'Database config', operation: 'Database info',
message: `Using database: ${dbConfig.database} on host: ${dbConfig.host}` message: {
version: versionResult.rows[0].version,
user: userResult.rows[0].current_user,
database: userResult.rows[0].current_database
}
}); });
// Get list of all tables in the current database // Get list of all tables in the current database
@@ -152,14 +148,14 @@ async function resetDatabase() {
message: 'Retrieving all table names...' message: 'Retrieving all table names...'
}); });
const [tables] = await connection.query(` const tablesResult = await client.query(`
SELECT GROUP_CONCAT(table_name) as tables SELECT string_agg(tablename, ', ') as tables
FROM information_schema.tables FROM pg_tables
WHERE table_schema = DATABASE() WHERE schemaname = 'public'
AND table_name NOT IN ('users', 'import_history', 'calculate_history') AND tablename NOT IN ('users', 'calculate_history', 'import_history');
`); `);
if (!tables[0].tables) { if (!tablesResult.rows[0].tables) {
outputProgress({ outputProgress({
operation: 'No tables found', operation: 'No tables found',
message: 'Database is already empty' message: 'Database is already empty'
@@ -170,20 +166,73 @@ async function resetDatabase() {
message: 'Dropping all existing tables...' message: 'Dropping all existing tables...'
}); });
await connection.query('SET FOREIGN_KEY_CHECKS = 0'); // Disable triggers/foreign key checks
const dropQuery = ` await client.query('SET session_replication_role = \'replica\';');
DROP TABLE IF EXISTS
${tables[0].tables // Drop all tables except users
.split(',') const tables = tablesResult.rows[0].tables.split(', ');
.filter(table => !['users', 'calculate_history'].includes(table)) for (const table of tables) {
.map(table => '`' + table + '`') if (!['users'].includes(table)) {
.join(', ')} await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`);
`; }
await connection.query(dropQuery); }
await connection.query('SET FOREIGN_KEY_CHECKS = 1');
// Only drop types if we're not preserving history tables
const historyTablesExist = await client.query(`
SELECT EXISTS (
SELECT FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('calculate_history', 'import_history')
);
`);
if (!historyTablesExist.rows[0].exists) {
await client.query('DROP TYPE IF EXISTS calculation_status CASCADE;');
await client.query('DROP TYPE IF EXISTS module_name CASCADE;');
}
// Re-enable triggers/foreign key checks
await client.query('SET session_replication_role = \'origin\';');
} }
// Read and execute main schema (core tables) // Create enum types if they don't exist
outputProgress({
operation: 'Creating enum types',
message: 'Setting up required enum types...'
});
// Check if types exist before creating
const typesExist = await client.query(`
SELECT EXISTS (
SELECT 1 FROM pg_type
WHERE typname = 'calculation_status'
) as calc_status_exists,
EXISTS (
SELECT 1 FROM pg_type
WHERE typname = 'module_name'
) as module_name_exists;
`);
if (!typesExist.rows[0].calc_status_exists) {
await client.query(`CREATE TYPE calculation_status AS ENUM ('running', 'completed', 'failed', 'cancelled')`);
}
if (!typesExist.rows[0].module_name_exists) {
await client.query(`
CREATE TYPE module_name AS ENUM (
'product_metrics',
'time_aggregates',
'financial_metrics',
'vendor_metrics',
'category_metrics',
'brand_metrics',
'sales_forecasts',
'abc_classification'
)
`);
}
// Read and execute main schema first (core tables)
outputProgress({ outputProgress({
operation: 'Running database setup', operation: 'Running database setup',
message: 'Creating core tables...' message: 'Creating core tables...'
@@ -223,35 +272,24 @@ async function resetDatabase() {
for (let i = 0; i < statements.length; i++) { for (let i = 0; i < statements.length; i++) {
const stmt = statements[i]; const stmt = statements[i];
try { try {
const [result, fields] = await connection.query(stmt); const result = await client.query(stmt);
// Check for warnings
const [warnings] = await connection.query('SHOW WARNINGS');
if (warnings && warnings.length > 0) {
outputProgress({
status: 'warning',
operation: 'SQL Warning',
statement: i + 1,
warnings: warnings
});
}
// Verify if table was created (if this was a CREATE TABLE statement) // Verify if table was created (if this was a CREATE TABLE statement)
if (stmt.trim().toLowerCase().startsWith('create table')) { if (stmt.trim().toLowerCase().startsWith('create table')) {
const tableName = stmt.match(/create\s+table\s+(?:if\s+not\s+exists\s+)?`?(\w+)`?/i)?.[1]; const tableName = stmt.match(/create\s+table\s+(?:if\s+not\s+exists\s+)?["]?(\w+)["]?/i)?.[1];
if (tableName) { if (tableName) {
const [tableExists] = await connection.query(` const tableExists = await client.query(`
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE table_schema = 'public'
AND table_name = ? AND table_name = $1
`, [tableName]); `, [tableName]);
outputProgress({ outputProgress({
operation: 'Table Creation Verification', operation: 'Table Creation Verification',
message: { message: {
table: tableName, table: tableName,
exists: tableExists[0].count > 0 exists: tableExists.rows[0].count > 0
} }
}); });
} }
@@ -263,7 +301,7 @@ async function resetDatabase() {
statement: i + 1, statement: i + 1,
total: statements.length, total: statements.length,
preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''), preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''),
affectedRows: result.affectedRows rowCount: result.rowCount
} }
}); });
} catch (sqlError) { } catch (sqlError) {
@@ -271,8 +309,6 @@ async function resetDatabase() {
status: 'error', status: 'error',
operation: 'SQL Error', operation: 'SQL Error',
error: sqlError.message, error: sqlError.message,
sqlState: sqlError.sqlState,
errno: sqlError.errno,
statement: stmt, statement: stmt,
statementNumber: i + 1 statementNumber: i + 1
}); });
@@ -280,66 +316,12 @@ async function resetDatabase() {
} }
} }
// List all tables in the database after schema execution // Verify core tables were created
outputProgress({ const existingTables = (await client.query(`
operation: 'Debug database', SELECT table_name
message: {
currentDatabase: (await connection.query('SELECT DATABASE() as db'))[0][0].db
}
});
const [allTables] = await connection.query(`
SELECT
table_schema,
table_name,
engine,
create_time,
table_rows
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = DATABASE() WHERE table_schema = 'public'
`); `)).rows.map(t => t.table_name);
if (allTables.length === 0) {
outputProgress({
operation: 'Warning',
message: 'No tables found in database after schema execution'
});
} else {
outputProgress({
operation: 'Tables after schema execution',
message: {
count: allTables.length,
tables: allTables.map(t => ({
schema: t.table_schema,
name: t.table_name,
engine: t.engine,
created: t.create_time,
rows: t.table_rows
}))
}
});
}
// Also check table status
const [tableStatus] = await connection.query('SHOW TABLE STATUS');
outputProgress({
operation: 'Table Status',
message: {
tables: tableStatus.map(t => ({
name: t.Name,
engine: t.Engine,
version: t.Version,
rowFormat: t.Row_format,
rows: t.Rows,
createTime: t.Create_time,
updateTime: t.Update_time
}))
}
});
// Verify core tables were created using SHOW TABLES
const [showTables] = await connection.query('SHOW TABLES');
const existingTables = showTables.map(t => Object.values(t)[0]);
outputProgress({ outputProgress({
operation: 'Core tables verification', operation: 'Core tables verification',
@@ -359,22 +341,12 @@ async function resetDatabase() {
); );
} }
// Verify all core tables use InnoDB
const [engineStatus] = await connection.query('SHOW TABLE STATUS WHERE Name IN (?)', [CORE_TABLES]);
const nonInnoDBTables = engineStatus.filter(t => t.Engine !== 'InnoDB');
if (nonInnoDBTables.length > 0) {
throw new Error(
`Tables using non-InnoDB engine: ${nonInnoDBTables.map(t => t.Name).join(', ')}`
);
}
outputProgress({ outputProgress({
operation: 'Core tables created', operation: 'Core tables created',
message: `Successfully created tables: ${CORE_TABLES.join(', ')}` message: `Successfully created tables: ${CORE_TABLES.join(', ')}`
}); });
// Read and execute config schema // Now read and execute config schema (since core tables exist)
outputProgress({ outputProgress({
operation: 'Running config setup', operation: 'Running config setup',
message: 'Creating configuration tables...' message: 'Creating configuration tables...'
@@ -400,18 +372,7 @@ async function resetDatabase() {
for (let i = 0; i < configStatements.length; i++) { for (let i = 0; i < configStatements.length; i++) {
const stmt = configStatements[i]; const stmt = configStatements[i];
try { try {
const [result, fields] = await connection.query(stmt); const result = await client.query(stmt);
// Check for warnings
const [warnings] = await connection.query('SHOW WARNINGS');
if (warnings && warnings.length > 0) {
outputProgress({
status: 'warning',
operation: 'Config SQL Warning',
statement: i + 1,
warnings: warnings
});
}
outputProgress({ outputProgress({
operation: 'Config SQL Progress', operation: 'Config SQL Progress',
@@ -419,7 +380,7 @@ async function resetDatabase() {
statement: i + 1, statement: i + 1,
total: configStatements.length, total: configStatements.length,
preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''), preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''),
affectedRows: result.affectedRows rowCount: result.rowCount
} }
}); });
} catch (sqlError) { } catch (sqlError) {
@@ -427,8 +388,6 @@ async function resetDatabase() {
status: 'error', status: 'error',
operation: 'Config SQL Error', operation: 'Config SQL Error',
error: sqlError.message, error: sqlError.message,
sqlState: sqlError.sqlState,
errno: sqlError.errno,
statement: stmt, statement: stmt,
statementNumber: i + 1 statementNumber: i + 1
}); });
@@ -436,33 +395,6 @@ async function resetDatabase() {
} }
} }
// Verify config tables were created
const [showConfigTables] = await connection.query('SHOW TABLES');
const existingConfigTables = showConfigTables.map(t => Object.values(t)[0]);
outputProgress({
operation: 'Config tables verification',
message: {
found: existingConfigTables,
expected: CONFIG_TABLES
}
});
const missingConfigTables = CONFIG_TABLES.filter(
t => !existingConfigTables.includes(t)
);
if (missingConfigTables.length > 0) {
throw new Error(
`Failed to create config tables: ${missingConfigTables.join(', ')}`
);
}
outputProgress({
operation: 'Config tables created',
message: `Successfully created tables: ${CONFIG_TABLES.join(', ')}`
});
// Read and execute metrics schema (metrics tables) // Read and execute metrics schema (metrics tables)
outputProgress({ outputProgress({
operation: 'Running metrics setup', operation: 'Running metrics setup',
@@ -489,18 +421,7 @@ async function resetDatabase() {
for (let i = 0; i < metricsStatements.length; i++) { for (let i = 0; i < metricsStatements.length; i++) {
const stmt = metricsStatements[i]; const stmt = metricsStatements[i];
try { try {
const [result, fields] = await connection.query(stmt); const result = await client.query(stmt);
// Check for warnings
const [warnings] = await connection.query('SHOW WARNINGS');
if (warnings && warnings.length > 0) {
outputProgress({
status: 'warning',
operation: 'Metrics SQL Warning',
statement: i + 1,
warnings: warnings
});
}
outputProgress({ outputProgress({
operation: 'Metrics SQL Progress', operation: 'Metrics SQL Progress',
@@ -508,7 +429,7 @@ async function resetDatabase() {
statement: i + 1, statement: i + 1,
total: metricsStatements.length, total: metricsStatements.length,
preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''), preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''),
affectedRows: result.affectedRows rowCount: result.rowCount
} }
}); });
} catch (sqlError) { } catch (sqlError) {
@@ -516,8 +437,6 @@ async function resetDatabase() {
status: 'error', status: 'error',
operation: 'Metrics SQL Error', operation: 'Metrics SQL Error',
error: sqlError.message, error: sqlError.message,
sqlState: sqlError.sqlState,
errno: sqlError.errno,
statement: stmt, statement: stmt,
statementNumber: i + 1 statementNumber: i + 1
}); });
@@ -539,7 +458,7 @@ async function resetDatabase() {
}); });
process.exit(1); process.exit(1);
} finally { } finally {
await connection.end(); await client.end();
} }
} }

View File

@@ -1,4 +1,4 @@
const mysql = require('mysql2/promise'); const { Client } = require('pg');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
@@ -8,7 +8,7 @@ const dbConfig = {
user: process.env.DB_USER, user: process.env.DB_USER,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
multipleStatements: true port: process.env.DB_PORT || 5432
}; };
function outputProgress(data) { function outputProgress(data) {
@@ -34,8 +34,8 @@ const METRICS_TABLES = [
'sales_forecasts', 'sales_forecasts',
'temp_purchase_metrics', 'temp_purchase_metrics',
'temp_sales_metrics', 'temp_sales_metrics',
'vendor_metrics', //before vendor_details for foreign key 'vendor_metrics',
'vendor_time_metrics', //before vendor_details for foreign key 'vendor_time_metrics',
'vendor_details' 'vendor_details'
]; ];
@@ -90,31 +90,31 @@ function splitSQLStatements(sql) {
} }
async function resetMetrics() { async function resetMetrics() {
let connection; let client;
try { try {
outputProgress({ outputProgress({
operation: 'Starting metrics reset', operation: 'Starting metrics reset',
message: 'Connecting to database...' message: 'Connecting to database...'
}); });
connection = await mysql.createConnection(dbConfig); client = new Client(dbConfig);
await connection.beginTransaction(); await client.connect();
// First verify current state // First verify current state
const [initialTables] = await connection.query(` const initialTables = await client.query(`
SELECT TABLE_NAME as name SELECT tablename as name
FROM information_schema.tables FROM pg_tables
WHERE TABLE_SCHEMA = DATABASE() WHERE schemaname = 'public'
AND TABLE_NAME IN (?) AND tablename = ANY($1)
`, [METRICS_TABLES]); `, [METRICS_TABLES]);
outputProgress({ outputProgress({
operation: 'Initial state', operation: 'Initial state',
message: `Found ${initialTables.length} existing metrics tables: ${initialTables.map(t => t.name).join(', ')}` message: `Found ${initialTables.rows.length} existing metrics tables: ${initialTables.rows.map(t => t.name).join(', ')}`
}); });
// Disable foreign key checks at the start // Disable foreign key checks at the start
await connection.query('SET FOREIGN_KEY_CHECKS = 0'); await client.query('SET session_replication_role = \'replica\'');
// Drop all metrics tables in reverse order to handle dependencies // Drop all metrics tables in reverse order to handle dependencies
outputProgress({ outputProgress({
@@ -124,17 +124,17 @@ async function resetMetrics() {
for (const table of [...METRICS_TABLES].reverse()) { for (const table of [...METRICS_TABLES].reverse()) {
try { try {
await connection.query(`DROP TABLE IF EXISTS ${table}`); await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`);
// Verify the table was actually dropped // Verify the table was actually dropped
const [checkDrop] = await connection.query(` const checkDrop = await client.query(`
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM information_schema.tables FROM pg_tables
WHERE TABLE_SCHEMA = DATABASE() WHERE schemaname = 'public'
AND TABLE_NAME = ? AND tablename = $1
`, [table]); `, [table]);
if (checkDrop[0].count > 0) { if (parseInt(checkDrop.rows[0].count) > 0) {
throw new Error(`Failed to drop table ${table} - table still exists`); throw new Error(`Failed to drop table ${table} - table still exists`);
} }
@@ -153,15 +153,15 @@ async function resetMetrics() {
} }
// Verify all tables were dropped // Verify all tables were dropped
const [afterDrop] = await connection.query(` const afterDrop = await client.query(`
SELECT TABLE_NAME as name SELECT tablename as name
FROM information_schema.tables FROM pg_tables
WHERE TABLE_SCHEMA = DATABASE() WHERE schemaname = 'public'
AND TABLE_NAME IN (?) AND tablename = ANY($1)
`, [METRICS_TABLES]); `, [METRICS_TABLES]);
if (afterDrop.length > 0) { if (afterDrop.rows.length > 0) {
throw new Error(`Failed to drop all tables. Remaining tables: ${afterDrop.map(t => t.name).join(', ')}`); throw new Error(`Failed to drop all tables. Remaining tables: ${afterDrop.rows.map(t => t.name).join(', ')}`);
} }
// Read metrics schema // Read metrics schema
@@ -187,39 +187,26 @@ async function resetMetrics() {
for (let i = 0; i < statements.length; i++) { for (let i = 0; i < statements.length; i++) {
const stmt = statements[i]; const stmt = statements[i];
try { try {
await connection.query(stmt); const result = await client.query(stmt);
// Check for warnings
const [warnings] = await connection.query('SHOW WARNINGS');
if (warnings && warnings.length > 0) {
outputProgress({
status: 'warning',
operation: 'SQL Warning',
message: {
statement: i + 1,
warnings: warnings
}
});
}
// If this is a CREATE TABLE statement, verify the table was created // If this is a CREATE TABLE statement, verify the table was created
if (stmt.trim().toLowerCase().startsWith('create table')) { if (stmt.trim().toLowerCase().startsWith('create table')) {
const tableName = stmt.match(/create\s+table\s+(?:if\s+not\s+exists\s+)?`?(\w+)`?/i)?.[1]; const tableName = stmt.match(/create\s+table\s+(?:if\s+not\s+exists\s+)?["]?(\w+)["]?/i)?.[1];
if (tableName) { if (tableName) {
const [checkCreate] = await connection.query(` const checkCreate = await client.query(`
SELECT TABLE_NAME as name, CREATE_TIME as created SELECT tablename as name
FROM information_schema.tables FROM pg_tables
WHERE TABLE_SCHEMA = DATABASE() WHERE schemaname = 'public'
AND TABLE_NAME = ? AND tablename = $1
`, [tableName]); `, [tableName]);
if (checkCreate.length === 0) { if (checkCreate.rows.length === 0) {
throw new Error(`Failed to create table ${tableName} - table does not exist after CREATE statement`); throw new Error(`Failed to create table ${tableName} - table does not exist after CREATE statement`);
} }
outputProgress({ outputProgress({
operation: 'Table created', operation: 'Table created',
message: `Successfully created table: ${tableName} at ${checkCreate[0].created}` message: `Successfully created table: ${tableName}`
}); });
} }
} }
@@ -229,7 +216,8 @@ async function resetMetrics() {
message: { message: {
statement: i + 1, statement: i + 1,
total: statements.length, total: statements.length,
preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : '') preview: stmt.substring(0, 100) + (stmt.length > 100 ? '...' : ''),
rowCount: result.rowCount
} }
}); });
} catch (sqlError) { } catch (sqlError) {
@@ -238,8 +226,6 @@ async function resetMetrics() {
operation: 'SQL Error', operation: 'SQL Error',
message: { message: {
error: sqlError.message, error: sqlError.message,
sqlState: sqlError.sqlState,
errno: sqlError.errno,
statement: stmt, statement: stmt,
statementNumber: i + 1 statementNumber: i + 1
} }
@@ -249,7 +235,7 @@ async function resetMetrics() {
} }
// Re-enable foreign key checks after all tables are created // Re-enable foreign key checks after all tables are created
await connection.query('SET FOREIGN_KEY_CHECKS = 1'); await client.query('SET session_replication_role = \'origin\'');
// Verify metrics tables were created // Verify metrics tables were created
outputProgress({ outputProgress({
@@ -257,37 +243,36 @@ async function resetMetrics() {
message: 'Checking all metrics tables were created...' message: 'Checking all metrics tables were created...'
}); });
const [metricsTablesResult] = await connection.query(` const metricsTablesResult = await client.query(`
SELECT SELECT tablename as name
TABLE_NAME as name, FROM pg_tables
TABLE_ROWS as \`rows\`, WHERE schemaname = 'public'
CREATE_TIME as created AND tablename = ANY($1)
FROM information_schema.tables
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME IN (?)
`, [METRICS_TABLES]); `, [METRICS_TABLES]);
outputProgress({ outputProgress({
operation: 'Tables found', operation: 'Tables found',
message: `Found ${metricsTablesResult.length} tables: ${metricsTablesResult.map(t => message: `Found ${metricsTablesResult.rows.length} tables: ${metricsTablesResult.rows.map(t => t.name).join(', ')}`
`${t.name} (created: ${t.created})`
).join(', ')}`
}); });
const existingMetricsTables = metricsTablesResult.map(t => t.name); const existingMetricsTables = metricsTablesResult.rows.map(t => t.name);
const missingMetricsTables = METRICS_TABLES.filter(t => !existingMetricsTables.includes(t)); const missingMetricsTables = METRICS_TABLES.filter(t => !existingMetricsTables.includes(t));
if (missingMetricsTables.length > 0) { if (missingMetricsTables.length > 0) {
// Do one final check of the actual tables // Do one final check of the actual tables
const [finalCheck] = await connection.query('SHOW TABLES'); const finalCheck = await client.query(`
SELECT tablename as name
FROM pg_tables
WHERE schemaname = 'public'
`);
outputProgress({ outputProgress({
operation: 'Final table check', operation: 'Final table check',
message: `All database tables: ${finalCheck.map(t => Object.values(t)[0]).join(', ')}` message: `All database tables: ${finalCheck.rows.map(t => t.name).join(', ')}`
}); });
throw new Error(`Failed to create metrics tables: ${missingMetricsTables.join(', ')}`); throw new Error(`Failed to create metrics tables: ${missingMetricsTables.join(', ')}`);
} }
await connection.commit(); await client.query('COMMIT');
outputProgress({ outputProgress({
status: 'complete', status: 'complete',
@@ -302,17 +287,17 @@ async function resetMetrics() {
stack: error.stack stack: error.stack
}); });
if (connection) { if (client) {
await connection.rollback(); await client.query('ROLLBACK');
// Make sure to re-enable foreign key checks even if there's an error // Make sure to re-enable foreign key checks even if there's an error
await connection.query('SET FOREIGN_KEY_CHECKS = 1').catch(() => {}); await client.query('SET session_replication_role = \'origin\'').catch(() => {});
} }
throw error; throw error;
} finally { } finally {
if (connection) { if (client) {
// One final attempt to ensure foreign key checks are enabled // One final attempt to ensure foreign key checks are enabled
await connection.query('SET FOREIGN_KEY_CHECKS = 1').catch(() => {}); await client.query('SET session_replication_role = \'origin\'').catch(() => {});
await connection.end(); await client.end();
} }
} }
} }