init
This commit is contained in:
commit
b15b95781c
108 changed files with 14802 additions and 0 deletions
112
migrations/0001_initial_schema.sql
Normal file
112
migrations/0001_initial_schema.sql
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
email TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(trim(email)) > 3),
|
||||
password_hash TEXT NOT NULL CHECK(length(password_hash) > 0),
|
||||
role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')),
|
||||
is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
last_login_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE projects (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL CHECK(length(trim(name)) > 0),
|
||||
slug TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(trim(slug)) > 0),
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL CHECK(length(trim(name)) > 0),
|
||||
slug TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(trim(slug)) > 0),
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE project_tags (
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
PRIMARY KEY (project_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE releases (
|
||||
id INTEGER PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
version TEXT NOT NULL CHECK(length(trim(version)) > 0),
|
||||
build TEXT NOT NULL DEFAULT '',
|
||||
filename TEXT NOT NULL CHECK(length(trim(filename)) > 0),
|
||||
storage_path TEXT NOT NULL UNIQUE CHECK(length(trim(storage_path)) > 0),
|
||||
checksum_sha256 TEXT NOT NULL CHECK(length(checksum_sha256) = 64),
|
||||
size_bytes INTEGER NOT NULL CHECK(size_bytes >= 0),
|
||||
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
||||
release_notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
uploaded_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)),
|
||||
UNIQUE (project_id, version, build)
|
||||
);
|
||||
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL CHECK(length(trim(name)) > 0),
|
||||
key_prefix TEXT NOT NULL UNIQUE CHECK(length(trim(key_prefix)) > 0),
|
||||
key_hash TEXT NOT NULL UNIQUE CHECK(length(trim(key_hash)) > 0),
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
scope_mode TEXT NOT NULL CHECK(scope_mode IN ('all_projects', 'project_allow_list', 'project_deny_list', 'tag_allow_list', 'tag_deny_list')),
|
||||
can_download INTEGER NOT NULL DEFAULT 0 CHECK(can_download IN (0, 1)),
|
||||
can_upload INTEGER NOT NULL DEFAULT 0 CHECK(can_upload IN (0, 1)),
|
||||
can_delete INTEGER NOT NULL DEFAULT 0 CHECK(can_delete IN (0, 1)),
|
||||
can_manage_projects INTEGER NOT NULL DEFAULT 0 CHECK(can_manage_projects IN (0, 1)),
|
||||
is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)),
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
last_used_at TEXT,
|
||||
created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE api_key_project_access (
|
||||
api_key_id INTEGER NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
PRIMARY KEY (api_key_id, project_id)
|
||||
);
|
||||
|
||||
CREATE TABLE api_key_tag_access (
|
||||
api_key_id INTEGER NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
PRIMARY KEY (api_key_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE CHECK(length(trim(token_hash)) > 0),
|
||||
expires_at TEXT NOT NULL,
|
||||
last_seen_at TEXT,
|
||||
invalidated_at TEXT,
|
||||
ip_address TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
api_key_id INTEGER REFERENCES api_keys(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL CHECK(length(trim(action)) > 0),
|
||||
target_type TEXT NOT NULL DEFAULT '',
|
||||
target_id INTEGER,
|
||||
target_identifier TEXT NOT NULL DEFAULT '',
|
||||
metadata_json TEXT NOT NULL DEFAULT '{}',
|
||||
ip_address TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
65
migrations/0002_indexes_and_triggers.sql
Normal file
65
migrations/0002_indexes_and_triggers.sql
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
CREATE INDEX idx_project_tags_tag_id ON project_tags(tag_id);
|
||||
|
||||
CREATE INDEX idx_releases_project_active_created_at ON releases(project_id, is_active, created_at DESC);
|
||||
CREATE INDEX idx_releases_uploaded_by_user_id ON releases(uploaded_by_user_id);
|
||||
|
||||
CREATE INDEX idx_api_keys_scope_mode_active ON api_keys(scope_mode, is_active);
|
||||
CREATE INDEX idx_api_key_project_access_project_id ON api_key_project_access(project_id);
|
||||
CREATE INDEX idx_api_key_tag_access_tag_id ON api_key_tag_access(tag_id);
|
||||
|
||||
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
||||
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
|
||||
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
|
||||
CREATE INDEX idx_audit_logs_actor_user_id ON audit_logs(actor_user_id);
|
||||
CREATE INDEX idx_audit_logs_api_key_id ON audit_logs(api_key_id);
|
||||
|
||||
CREATE TRIGGER users_touch_updated_at
|
||||
AFTER UPDATE ON users
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE users
|
||||
SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER projects_touch_updated_at
|
||||
AFTER UPDATE ON projects
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE projects
|
||||
SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER tags_touch_updated_at
|
||||
AFTER UPDATE ON tags
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE tags
|
||||
SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER releases_touch_updated_at
|
||||
AFTER UPDATE ON releases
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE releases
|
||||
SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER api_keys_touch_updated_at
|
||||
AFTER UPDATE ON api_keys
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE api_keys
|
||||
SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
52
migrations/0003_api_key_scope_guards.sql
Normal file
52
migrations/0003_api_key_scope_guards.sql
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
CREATE TRIGGER api_key_project_access_scope_guard
|
||||
BEFORE INSERT ON api_key_project_access
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT scope_mode FROM api_keys WHERE id = NEW.api_key_id) NOT IN ('project_allow_list', 'project_deny_list')
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'api key scope_mode does not permit project access rows');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER api_key_tag_access_scope_guard
|
||||
BEFORE INSERT ON api_key_tag_access
|
||||
FOR EACH ROW
|
||||
WHEN (SELECT scope_mode FROM api_keys WHERE id = NEW.api_key_id) NOT IN ('tag_allow_list', 'tag_deny_list')
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'api key scope_mode does not permit tag access rows');
|
||||
END;
|
||||
|
||||
CREATE TRIGGER api_keys_scope_mode_transition_guard
|
||||
BEFORE UPDATE OF scope_mode ON api_keys
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT CASE
|
||||
WHEN NEW.scope_mode = 'all_projects' AND EXISTS (
|
||||
SELECT 1
|
||||
FROM api_key_project_access
|
||||
WHERE api_key_id = NEW.id
|
||||
) THEN RAISE(ABORT, 'all_projects keys cannot keep project access rows')
|
||||
END;
|
||||
|
||||
SELECT CASE
|
||||
WHEN NEW.scope_mode = 'all_projects' AND EXISTS (
|
||||
SELECT 1
|
||||
FROM api_key_tag_access
|
||||
WHERE api_key_id = NEW.id
|
||||
) THEN RAISE(ABORT, 'all_projects keys cannot keep tag access rows')
|
||||
END;
|
||||
|
||||
SELECT CASE
|
||||
WHEN NEW.scope_mode IN ('project_allow_list', 'project_deny_list') AND EXISTS (
|
||||
SELECT 1
|
||||
FROM api_key_tag_access
|
||||
WHERE api_key_id = NEW.id
|
||||
) THEN RAISE(ABORT, 'project-scoped keys cannot keep tag access rows')
|
||||
END;
|
||||
|
||||
SELECT CASE
|
||||
WHEN NEW.scope_mode IN ('tag_allow_list', 'tag_deny_list') AND EXISTS (
|
||||
SELECT 1
|
||||
FROM api_key_project_access
|
||||
WHERE api_key_id = NEW.id
|
||||
) THEN RAISE(ABORT, 'tag-scoped keys cannot keep project access rows')
|
||||
END;
|
||||
END;
|
||||
5
migrations/README.md
Normal file
5
migrations/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Migrations
|
||||
|
||||
This directory stores the ordered SQLite migration files applied by the app and by `go run ./cmd/migrate`.
|
||||
|
||||
Files are applied lexicographically and tracked in the `schema_migrations` table with a SHA-256 checksum so edited applied migrations fail fast.
|
||||
Loading…
Add table
Add a link
Reference in a new issue