Initial commit

This commit is contained in:
2026-06-01 16:48:21 +02:00
commit 53d157535a
159 changed files with 13641 additions and 0 deletions
View File
+57
View File
@@ -0,0 +1,57 @@
"""Alembic environment — runs raw SQL migrations against SQLite or Postgres.
No SQLAlchemy ORM is used. Migrations are plain SQL scripts so the migration
layer stays independent of any Python model objects.
"""
from __future__ import annotations
import asyncio
from logging.config import fileConfig
from typing import Any
from alembic import context
# Load the alembic.ini logging config.
if context.config.config_file_name is not None:
fileConfig(context.config.config_file_name)
def run_migrations_offline() -> None:
"""Emit SQL to stdout (used by `alembic upgrade --sql`)."""
url = context.config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=None,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Any) -> None:
context.configure(connection=connection, target_metadata=None)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online() -> None:
"""Run migrations against a live DB connection."""
from sqlalchemy.ext.asyncio import create_async_engine
url = context.config.get_main_option("sqlalchemy.url")
# Coerce sqlite:// → sqlite+aiosqlite:// for async support.
if url and url.startswith("sqlite:///"):
url = url.replace("sqlite:///", "sqlite+aiosqlite:///", 1)
connectable = create_async_engine(url)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())
+23
View File
@@ -0,0 +1,23 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: str | None = ${repr(down_revision)}
branch_labels: str | None = ${repr(branch_labels)}
depends_on: str | None = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,156 @@
"""Initial schema — all nine tables for the full application.
Revision ID: 001
Revises: None
Create Date: 2026-05-31
"""
from __future__ import annotations
from alembic import op
revision: str = "001"
down_revision: str | None = None
branch_labels: str | None = None
depends_on: str | None = None
# Tables are created in FK-dependency order (workspace first).
_UPGRADE_SQL = """
CREATE TABLE IF NOT EXISTS workspace (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
planning_goal TEXT NOT NULL DEFAULT 'balanced'
CHECK (planning_goal IN ('max_time','max_money','balanced')),
created_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS personal_data (
uuid TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
name TEXT NOT NULL,
salary_type TEXT NOT NULL DEFAULT 'monthly'
CHECK (salary_type IN ('monthly','hourly')),
employment_status TEXT NOT NULL DEFAULT 'employed'
CHECK (employment_status IN ('employed','jobseeking','none')),
personnummer_enc TEXT,
created_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP,
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "user" (
id TEXT PRIMARY KEY,
personal_data_uuid TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS workspace_invitation (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
invited_by_user_id TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('pending','accepted','revoked','expired')),
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL,
accepted_at TIMESTAMP,
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE,
FOREIGN KEY (invited_by_user_id) REFERENCES "user"(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS child (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
name TEXT NOT NULL,
birth_date DATE NOT NULL,
preschool_start_date DATE,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS break_period (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
name TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
target_weeks INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS parental_leave_fetch (
id TEXT PRIMARY KEY,
personal_data_uuid TEXT NOT NULL,
workspace_id TEXT NOT NULL,
child_id TEXT NOT NULL,
source TEXT NOT NULL CHECK (source IN ('scraped','manual')),
fetched_at TIMESTAMP NOT NULL,
total_days INTEGER NOT NULL,
days_used INTEGER NOT NULL,
days_left INTEGER NOT NULL,
reserved_days_total INTEGER NOT NULL DEFAULT 90,
reserved_days_left INTEGER NOT NULL,
transferable_days_left INTEGER NOT NULL,
dubbeldagar_left INTEGER NOT NULL,
tier_data TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE,
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE,
FOREIGN KEY (child_id) REFERENCES child(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS vacation_days (
id TEXT PRIMARY KEY,
personal_data_uuid TEXT NOT NULL,
workspace_id TEXT NOT NULL,
year INTEGER NOT NULL,
total_days INTEGER NOT NULL,
saved_days_in INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(personal_data_uuid, year),
FOREIGN KEY (personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE,
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS planned_leave_day (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
child_id TEXT NOT NULL,
parent_personal_data_uuid TEXT NOT NULL,
date DATE NOT NULL,
day_type TEXT NOT NULL CHECK (day_type IN ('sjukpenning','lagsta','vacation')),
is_dubbeldag INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(workspace_id, child_id, date, parent_personal_data_uuid),
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE,
FOREIGN KEY (child_id) REFERENCES child(id) ON DELETE CASCADE,
FOREIGN KEY (parent_personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE
);
"""
_DOWNGRADE_SQL = """
DROP TABLE IF EXISTS planned_leave_day;
DROP TABLE IF EXISTS vacation_days;
DROP TABLE IF EXISTS parental_leave_fetch;
DROP TABLE IF EXISTS break_period;
DROP TABLE IF EXISTS child;
DROP TABLE IF EXISTS workspace_invitation;
DROP TABLE IF EXISTS "user";
DROP TABLE IF EXISTS personal_data;
DROP TABLE IF EXISTS workspace;
"""
def upgrade() -> None:
op.execute(_UPGRADE_SQL)
def downgrade() -> None:
op.execute(_DOWNGRADE_SQL)