feat: Consolidate user, role, and theme settings tables into a single migration file
This commit is contained in:
@@ -158,4 +158,32 @@ ALTER TABLE capex
|
|||||||
ALTER TABLE opex
|
ALTER TABLE opex
|
||||||
DROP COLUMN IF EXISTS currency_code;
|
DROP COLUMN IF EXISTS currency_code;
|
||||||
|
|
||||||
|
-- Role-based access control tables
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) UNIQUE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
hashed_password VARCHAR(255) NOT NULL,
|
||||||
|
role_id INTEGER NOT NULL REFERENCES roles (id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_users_username ON users (username);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_users_email ON users (email);
|
||||||
|
|
||||||
|
-- Theme settings configuration table
|
||||||
|
CREATE TABLE IF NOT EXISTS theme_settings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
theme_name VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
primary_color VARCHAR(7) NOT NULL,
|
||||||
|
secondary_color VARCHAR(7) NOT NULL,
|
||||||
|
accent_color VARCHAR(7) NOT NULL,
|
||||||
|
background_color VARCHAR(7) NOT NULL,
|
||||||
|
text_color VARCHAR(7) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
-- Migration: Create application_setting table for configurable application options
|
|
||||||
-- Date: 2025-10-25
|
|
||||||
-- Description: Introduces persistent storage for application-level settings such as theme colors.
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS application_setting (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
key VARCHAR(128) NOT NULL UNIQUE,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
value_type VARCHAR(32) NOT NULL DEFAULT 'string',
|
|
||||||
category VARCHAR(32) NOT NULL DEFAULT 'general',
|
|
||||||
description TEXT,
|
|
||||||
is_editable BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_application_setting_key
|
|
||||||
ON application_setting (key);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS ix_application_setting_category
|
|
||||||
ON application_setting (category);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Migration: 20251027_create_theme_settings_table.sql
|
|
||||||
|
|
||||||
CREATE TABLE theme_settings (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
theme_name VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
primary_color VARCHAR(7) NOT NULL,
|
|
||||||
secondary_color VARCHAR(7) NOT NULL,
|
|
||||||
accent_color VARCHAR(7) NOT NULL,
|
|
||||||
background_color VARCHAR(7) NOT NULL,
|
|
||||||
text_color VARCHAR(7) NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
-- Migration: 20251027_create_user_and_role_tables.sql
|
|
||||||
|
|
||||||
CREATE TABLE roles (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) UNIQUE NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
hashed_password VARCHAR(255) NOT NULL,
|
|
||||||
role_id INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (role_id) REFERENCES roles(id)
|
|
||||||
);
|
|
||||||
@@ -22,6 +22,7 @@ connection string; this script will still honor the granular inputs above.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from config.database import Base
|
||||||
import argparse
|
import argparse
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
@@ -43,7 +44,6 @@ from sqlalchemy import create_engine, inspect
|
|||||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||||
if str(ROOT_DIR) not in sys.path:
|
if str(ROOT_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(ROOT_DIR))
|
sys.path.insert(0, str(ROOT_DIR))
|
||||||
from config.database import Base
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -126,7 +126,8 @@ class DatabaseConfig:
|
|||||||
]
|
]
|
||||||
if missing:
|
if missing:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Missing required database configuration: " + ", ".join(missing)
|
"Missing required database configuration: " +
|
||||||
|
", ".join(missing)
|
||||||
)
|
)
|
||||||
|
|
||||||
host = cast(str, host)
|
host = cast(str, host)
|
||||||
@@ -340,7 +341,8 @@ class DatabaseSetup:
|
|||||||
rollback_label = f"drop database {self.config.database}"
|
rollback_label = f"drop database {self.config.database}"
|
||||||
self._register_rollback(
|
self._register_rollback(
|
||||||
rollback_label,
|
rollback_label,
|
||||||
lambda db=self.config.database: self._drop_database(db),
|
lambda db=self.config.database: self._drop_database(
|
||||||
|
db),
|
||||||
)
|
)
|
||||||
logger.info("Created database '%s'", self.config.database)
|
logger.info("Created database '%s'", self.config.database)
|
||||||
finally:
|
finally:
|
||||||
@@ -409,7 +411,8 @@ class DatabaseSetup:
|
|||||||
rollback_label = f"drop role {self.config.user}"
|
rollback_label = f"drop role {self.config.user}"
|
||||||
self._register_rollback(
|
self._register_rollback(
|
||||||
rollback_label,
|
rollback_label,
|
||||||
lambda role=self.config.user: self._drop_role(role),
|
lambda role=self.config.user: self._drop_role(
|
||||||
|
role),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Role '%s' already present", self.config.user)
|
logger.info("Role '%s' already present", self.config.user)
|
||||||
@@ -839,6 +842,7 @@ class DatabaseSetup:
|
|||||||
seed_args = argparse.Namespace(
|
seed_args = argparse.Namespace(
|
||||||
currencies=True,
|
currencies=True,
|
||||||
units=True,
|
units=True,
|
||||||
|
theme=True,
|
||||||
defaults=False,
|
defaults=False,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
verbose=0,
|
verbose=0,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ def test_seed_baseline_data_dry_run_skips_verification(
|
|||||||
assert namespace_arg.dry_run is True
|
assert namespace_arg.dry_run is True
|
||||||
assert namespace_arg.currencies is True
|
assert namespace_arg.currencies is True
|
||||||
assert namespace_arg.units is True
|
assert namespace_arg.units is True
|
||||||
|
assert namespace_arg.theme is True
|
||||||
assert seed_run.call_args.kwargs["config"] is setup_instance.config
|
assert seed_run.call_args.kwargs["config"] is setup_instance.config
|
||||||
verify_mock.assert_not_called()
|
verify_mock.assert_not_called()
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ def test_seed_baseline_data_invokes_verification(
|
|||||||
assert isinstance(namespace_arg, argparse.Namespace)
|
assert isinstance(namespace_arg, argparse.Namespace)
|
||||||
assert namespace_arg.dry_run is False
|
assert namespace_arg.dry_run is False
|
||||||
assert seed_run.call_args.kwargs["config"] is setup_instance.config
|
assert seed_run.call_args.kwargs["config"] is setup_instance.config
|
||||||
|
assert namespace_arg.theme is True
|
||||||
verify_mock.assert_called_once_with(
|
verify_mock.assert_called_once_with(
|
||||||
expected_currency_codes=expected_currencies,
|
expected_currency_codes=expected_currencies,
|
||||||
expected_unit_codes=expected_units,
|
expected_unit_codes=expected_units,
|
||||||
|
|||||||
Reference in New Issue
Block a user