refactor(backend): split monolithic migration into 7 goose files
001_init_schemas — extensions, schemas, uuid_v7 functions 002_core_tables — core.users, mime_types, object_types 003_data_tables — data.categories, tags, tag_rules, files, file_tag, pools, file_pool 004_acl_tables — acl.permissions 005_activity_tables — activity.action_types, sessions, file_views, pool_views, tag_uses, audit_log 006_indexes — all indexes across all schemas 007_seed_data — object_types and action_types reference rows Each file has -- +goose Up / Down annotations; downs drop in reverse dependency order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a2823337b6
commit
ecad017274
@ -1,425 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- Tanabata File Manager — Database Schema v2
|
||||
-- =============================================================================
|
||||
-- PostgreSQL 14+
|
||||
--
|
||||
-- Design decisions:
|
||||
-- • Business logic lives in Go (DDD), no stored procedures
|
||||
-- • UUID v7 for entity PKs (created_at extracted from UUID, no separate column)
|
||||
-- • ACL: is_public flag on objects + acl.permissions table for granular control
|
||||
-- • Schemas: core, data, acl, activity
|
||||
-- • Flat pools (no hierarchy)
|
||||
-- • Soft delete for files only (trash/recycle bin)
|
||||
-- • phash field for future duplicate detection
|
||||
-- • metadata jsonb on all entities
|
||||
-- • Unified audit log with reference tables instead of enums
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Extensions
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Schemas
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS core;
|
||||
CREATE SCHEMA IF NOT EXISTS data;
|
||||
CREATE SCHEMA IF NOT EXISTS acl;
|
||||
CREATE SCHEMA IF NOT EXISTS activity;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Utility functions
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- UUID v7 generator
|
||||
CREATE OR REPLACE FUNCTION public.uuid_v7(cts timestamptz DEFAULT clock_timestamp())
|
||||
RETURNS uuid LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
state text = current_setting('uuidv7.old_tp', true);
|
||||
old_tp text = split_part(state, ':', 1);
|
||||
base int = coalesce(nullif(split_part(state, ':', 4), '')::int, (random()*16777215/2-1)::int);
|
||||
tp text;
|
||||
entropy text;
|
||||
seq text = base;
|
||||
seqn int = split_part(state, ':', 2);
|
||||
ver text = coalesce(split_part(state, ':', 3), to_hex(8+(random()*3)::int));
|
||||
BEGIN
|
||||
base = (random()*16777215/2-1)::int;
|
||||
tp = lpad(to_hex(floor(extract(epoch from cts)*1000)::int8), 12, '0') || '7';
|
||||
IF tp IS DISTINCT FROM old_tp THEN
|
||||
old_tp = tp;
|
||||
ver = to_hex(8+(random()*3)::int);
|
||||
base = (random()*16777215/2-1)::int;
|
||||
seqn = base;
|
||||
ELSE
|
||||
seqn = seqn + (random()*1000)::int;
|
||||
END IF;
|
||||
PERFORM set_config('uuidv7.old_tp', old_tp||':'||seqn||':'||ver||':'||base, false);
|
||||
entropy = md5(gen_random_uuid()::text);
|
||||
seq = lpad(to_hex(seqn), 6, '0');
|
||||
RETURN (tp || substring(seq from 1 for 3) || ver || substring(seq from 4 for 3) ||
|
||||
substring(entropy from 1 for 12))::uuid;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Extract timestamp from UUID v7
|
||||
CREATE OR REPLACE FUNCTION public.uuid_extract_timestamp(uuid_val uuid)
|
||||
RETURNS timestamptz LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
|
||||
SELECT to_timestamp(
|
||||
('x' || left(replace(uuid_val::text, '-', ''), 12))::bit(48)::bigint / 1000.0
|
||||
);
|
||||
$$;
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: core
|
||||
-- =============================================================================
|
||||
|
||||
-- Users
|
||||
CREATE TABLE core.users (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(32) NOT NULL,
|
||||
password text NOT NULL, -- bcrypt hash via pgcrypto
|
||||
is_admin boolean NOT NULL DEFAULT false,
|
||||
can_create boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__users__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- MIME types (whitelist of supported file types)
|
||||
CREATE TABLE core.mime_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(127) NOT NULL,
|
||||
extension varchar(16) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__mime_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Object types (file, tag, category, pool — used in ACL and audit log)
|
||||
CREATE TABLE core.object_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(32) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__object_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: data
|
||||
-- =============================================================================
|
||||
|
||||
-- Categories (logical grouping of tags)
|
||||
CREATE TABLE data.categories (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
color char(6),
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__categories__name UNIQUE (name),
|
||||
CONSTRAINT chk__categories__color_hex
|
||||
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
|
||||
);
|
||||
|
||||
-- Tags
|
||||
CREATE TABLE data.tags (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
color char(6),
|
||||
category_id uuid REFERENCES data.categories(id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL,
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__tags__name UNIQUE (name),
|
||||
CONSTRAINT chk__tags__color_hex
|
||||
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
|
||||
);
|
||||
|
||||
-- Tag rules (when when_tag is added to a file, then_tag is also added)
|
||||
CREATE TABLE data.tag_rules (
|
||||
when_tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
then_tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
|
||||
PRIMARY KEY (when_tag_id, then_tag_id)
|
||||
);
|
||||
|
||||
-- Files
|
||||
CREATE TABLE data.files (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
original_name varchar(256), -- original filename at upload time
|
||||
mime_id smallint NOT NULL REFERENCES core.mime_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken)
|
||||
notes text,
|
||||
metadata jsonb, -- user-editable key-value data
|
||||
exif jsonb NOT NULL, -- EXIF data extracted at upload (immutable)
|
||||
phash bigint, -- perceptual hash for duplicate detection (future)
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
is_deleted boolean NOT NULL DEFAULT false -- soft delete (trash)
|
||||
);
|
||||
|
||||
-- File ↔ Tag (many-to-many)
|
||||
CREATE TABLE data.file_tag (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
|
||||
PRIMARY KEY (file_id, tag_id)
|
||||
);
|
||||
|
||||
-- Pools (ordered collections of files)
|
||||
CREATE TABLE data.pools (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__pools__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- File ↔ Pool (many-to-many, with ordering)
|
||||
-- `position` uses integer with gaps (e.g. 1000, 2000, 3000) to allow
|
||||
-- insertions without renumbering. Compact when gaps get too small.
|
||||
CREATE TABLE data.file_pool (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
pool_id uuid NOT NULL REFERENCES data.pools(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (file_id, pool_id)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: acl
|
||||
-- =============================================================================
|
||||
|
||||
-- Granular permissions
|
||||
-- If is_public=true on the object, it is accessible to everyone (ACL ignored).
|
||||
-- If is_public=false, only creator and users with can_view=true see it.
|
||||
-- Admins bypass all ACL checks.
|
||||
CREATE TABLE acl.permissions (
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
object_type_id smallint NOT NULL REFERENCES core.object_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_id uuid NOT NULL,
|
||||
can_view boolean NOT NULL DEFAULT true,
|
||||
can_edit boolean NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (user_id, object_type_id, object_id)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SCHEMA: activity
|
||||
-- =============================================================================
|
||||
|
||||
-- Action types (reference table for audit log)
|
||||
CREATE TABLE activity.action_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(64) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__action_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Sessions
|
||||
CREATE TABLE activity.sessions (
|
||||
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
token_hash text NOT NULL, -- hashed session token
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_agent varchar(256) NOT NULL,
|
||||
started_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
expires_at timestamptz,
|
||||
last_activity timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
CONSTRAINT uni__sessions__token_hash UNIQUE (token_hash)
|
||||
);
|
||||
|
||||
-- File views (analytics)
|
||||
CREATE TABLE activity.file_views (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
viewed_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
PRIMARY KEY (file_id, viewed_at, user_id)
|
||||
);
|
||||
|
||||
-- Pool views (analytics)
|
||||
CREATE TABLE activity.pool_views (
|
||||
pool_id uuid NOT NULL REFERENCES data.pools(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
viewed_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
PRIMARY KEY (pool_id, viewed_at, user_id)
|
||||
);
|
||||
|
||||
-- Tag usage tracking (when tag is used as filter)
|
||||
CREATE TABLE activity.tag_uses (
|
||||
tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
used_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
is_included boolean NOT NULL, -- true=included in filter, false=excluded
|
||||
|
||||
PRIMARY KEY (tag_id, used_at, user_id)
|
||||
);
|
||||
|
||||
-- Audit log (unified journal for all user actions)
|
||||
CREATE TABLE activity.audit_log (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
action_type_id smallint NOT NULL REFERENCES activity.action_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_type_id smallint REFERENCES core.object_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_id uuid,
|
||||
details jsonb, -- action-specific payload
|
||||
performed_at timestamptz NOT NULL DEFAULT statement_timestamp()
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- SEED DATA
|
||||
-- =============================================================================
|
||||
|
||||
-- Object types
|
||||
INSERT INTO core.object_types (name) VALUES
|
||||
('file'), ('tag'), ('category'), ('pool');
|
||||
|
||||
-- Action types
|
||||
INSERT INTO activity.action_types (name) VALUES
|
||||
-- Auth
|
||||
('user_login'), ('user_logout'),
|
||||
-- Files
|
||||
('file_create'), ('file_edit'), ('file_delete'), ('file_restore'),
|
||||
('file_permanent_delete'), ('file_replace'),
|
||||
-- Tags
|
||||
('tag_create'), ('tag_edit'), ('tag_delete'),
|
||||
-- Categories
|
||||
('category_create'), ('category_edit'), ('category_delete'),
|
||||
-- Pools
|
||||
('pool_create'), ('pool_edit'), ('pool_delete'),
|
||||
-- Relations
|
||||
('file_tag_add'), ('file_tag_remove'),
|
||||
('file_pool_add'), ('file_pool_remove'),
|
||||
-- ACL
|
||||
('acl_change'),
|
||||
-- Admin
|
||||
('user_create'), ('user_delete'), ('user_block'), ('user_unblock'),
|
||||
('user_role_change'),
|
||||
-- Sessions
|
||||
('session_terminate');
|
||||
|
||||
-- =============================================================================
|
||||
-- INDEXES
|
||||
-- =============================================================================
|
||||
|
||||
-- core
|
||||
CREATE INDEX idx__users__name ON core.users USING hash (name);
|
||||
|
||||
-- data.categories
|
||||
CREATE INDEX idx__categories__creator_id ON data.categories USING hash (creator_id);
|
||||
|
||||
-- data.tags
|
||||
CREATE INDEX idx__tags__category_id ON data.tags USING hash (category_id);
|
||||
CREATE INDEX idx__tags__creator_id ON data.tags USING hash (creator_id);
|
||||
|
||||
-- data.tag_rules
|
||||
CREATE INDEX idx__tag_rules__when ON data.tag_rules USING hash (when_tag_id);
|
||||
CREATE INDEX idx__tag_rules__then ON data.tag_rules USING hash (then_tag_id);
|
||||
|
||||
-- data.files
|
||||
CREATE INDEX idx__files__mime_id ON data.files USING hash (mime_id);
|
||||
CREATE INDEX idx__files__creator_id ON data.files USING hash (creator_id);
|
||||
CREATE INDEX idx__files__content_datetime ON data.files USING btree (content_datetime DESC NULLS LAST);
|
||||
CREATE INDEX idx__files__is_deleted ON data.files USING btree (is_deleted) WHERE is_deleted = true;
|
||||
CREATE INDEX idx__files__phash ON data.files USING btree (phash) WHERE phash IS NOT NULL;
|
||||
|
||||
-- data.file_tag
|
||||
CREATE INDEX idx__file_tag__tag_id ON data.file_tag USING hash (tag_id);
|
||||
CREATE INDEX idx__file_tag__file_id ON data.file_tag USING hash (file_id);
|
||||
|
||||
-- data.pools
|
||||
CREATE INDEX idx__pools__creator_id ON data.pools USING hash (creator_id);
|
||||
|
||||
-- data.file_pool
|
||||
CREATE INDEX idx__file_pool__pool_id ON data.file_pool USING hash (pool_id);
|
||||
CREATE INDEX idx__file_pool__file_id ON data.file_pool USING hash (file_id);
|
||||
|
||||
-- acl.permissions
|
||||
CREATE INDEX idx__acl__object ON acl.permissions USING btree (object_type_id, object_id);
|
||||
CREATE INDEX idx__acl__user ON acl.permissions USING hash (user_id);
|
||||
|
||||
-- activity.sessions
|
||||
CREATE INDEX idx__sessions__user_id ON activity.sessions USING hash (user_id);
|
||||
CREATE INDEX idx__sessions__token_hash ON activity.sessions USING hash (token_hash);
|
||||
|
||||
-- activity.file_views
|
||||
CREATE INDEX idx__file_views__user_id ON activity.file_views USING hash (user_id);
|
||||
|
||||
-- activity.pool_views
|
||||
CREATE INDEX idx__pool_views__user_id ON activity.pool_views USING hash (user_id);
|
||||
|
||||
-- activity.tag_uses
|
||||
CREATE INDEX idx__tag_uses__user_id ON activity.tag_uses USING hash (user_id);
|
||||
|
||||
-- activity.audit_log
|
||||
CREATE INDEX idx__audit_log__user_id ON activity.audit_log USING hash (user_id);
|
||||
CREATE INDEX idx__audit_log__action_type_id ON activity.audit_log USING hash (action_type_id);
|
||||
CREATE INDEX idx__audit_log__object ON activity.audit_log USING btree (object_type_id, object_id)
|
||||
WHERE object_id IS NOT NULL;
|
||||
CREATE INDEX idx__audit_log__performed_at ON activity.audit_log USING btree (performed_at DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- COMMENTS
|
||||
-- =============================================================================
|
||||
|
||||
COMMENT ON TABLE core.users IS 'Application users';
|
||||
COMMENT ON TABLE core.mime_types IS 'Whitelist of supported MIME types';
|
||||
COMMENT ON TABLE core.object_types IS 'Reference: entity types for ACL and audit log';
|
||||
COMMENT ON TABLE data.categories IS 'Logical grouping of tags';
|
||||
COMMENT ON TABLE data.tags IS 'File labels/tags';
|
||||
COMMENT ON TABLE data.tag_rules IS 'Auto-tagging rules: when when_tag is assigned, then_tag follows';
|
||||
COMMENT ON TABLE data.files IS 'Managed files; actual content stored on disk as {id}.{ext}';
|
||||
COMMENT ON TABLE data.file_tag IS 'Many-to-many: files <-> tags';
|
||||
COMMENT ON TABLE data.pools IS 'Ordered collections of files';
|
||||
COMMENT ON TABLE data.file_pool IS 'Many-to-many: files <-> pools, with ordering';
|
||||
COMMENT ON TABLE acl.permissions IS 'Per-object permissions (used when is_public=false)';
|
||||
COMMENT ON TABLE activity.action_types IS 'Reference: types of auditable user actions';
|
||||
COMMENT ON TABLE activity.sessions IS 'Active user sessions';
|
||||
COMMENT ON TABLE activity.file_views IS 'File view history';
|
||||
COMMENT ON TABLE activity.pool_views IS 'Pool view history';
|
||||
COMMENT ON TABLE activity.tag_uses IS 'Tag usage in filters';
|
||||
COMMENT ON TABLE activity.audit_log IS 'Unified audit trail for all user actions';
|
||||
|
||||
COMMENT ON COLUMN data.files.original_name IS 'Original filename at upload time';
|
||||
COMMENT ON COLUMN data.files.content_datetime IS 'Content datetime (e.g. when photo was taken); falls back to EXIF DateTimeOriginal';
|
||||
COMMENT ON COLUMN data.files.metadata IS 'User-editable key-value metadata';
|
||||
COMMENT ON COLUMN data.files.exif IS 'EXIF data extracted at upload time (immutable, system-managed)';
|
||||
COMMENT ON COLUMN data.files.phash IS 'Perceptual hash for image/video duplicate detection';
|
||||
COMMENT ON COLUMN data.files.is_deleted IS 'Soft-deleted files (trash); true = in recycle bin';
|
||||
COMMENT ON COLUMN data.file_pool.position IS 'Manual ordering within pool; uses gapped integers';
|
||||
61
backend/migrations/001_init_schemas.sql
Normal file
61
backend/migrations/001_init_schemas.sql
Normal file
@ -0,0 +1,61 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS core;
|
||||
CREATE SCHEMA IF NOT EXISTS data;
|
||||
CREATE SCHEMA IF NOT EXISTS acl;
|
||||
CREATE SCHEMA IF NOT EXISTS activity;
|
||||
|
||||
-- UUID v7 generator
|
||||
CREATE OR REPLACE FUNCTION public.uuid_v7(cts timestamptz DEFAULT clock_timestamp())
|
||||
RETURNS uuid LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
state text = current_setting('uuidv7.old_tp', true);
|
||||
old_tp text = split_part(state, ':', 1);
|
||||
base int = coalesce(nullif(split_part(state, ':', 4), '')::int, (random()*16777215/2-1)::int);
|
||||
tp text;
|
||||
entropy text;
|
||||
seq text = base;
|
||||
seqn int = split_part(state, ':', 2);
|
||||
ver text = coalesce(split_part(state, ':', 3), to_hex(8+(random()*3)::int));
|
||||
BEGIN
|
||||
base = (random()*16777215/2-1)::int;
|
||||
tp = lpad(to_hex(floor(extract(epoch from cts)*1000)::int8), 12, '0') || '7';
|
||||
IF tp IS DISTINCT FROM old_tp THEN
|
||||
old_tp = tp;
|
||||
ver = to_hex(8+(random()*3)::int);
|
||||
base = (random()*16777215/2-1)::int;
|
||||
seqn = base;
|
||||
ELSE
|
||||
seqn = seqn + (random()*1000)::int;
|
||||
END IF;
|
||||
PERFORM set_config('uuidv7.old_tp', old_tp||':'||seqn||':'||ver||':'||base, false);
|
||||
entropy = md5(gen_random_uuid()::text);
|
||||
seq = lpad(to_hex(seqn), 6, '0');
|
||||
RETURN (tp || substring(seq from 1 for 3) || ver || substring(seq from 4 for 3) ||
|
||||
substring(entropy from 1 for 12))::uuid;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Extract timestamp from UUID v7
|
||||
CREATE OR REPLACE FUNCTION public.uuid_extract_timestamp(uuid_val uuid)
|
||||
RETURNS timestamptz LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
|
||||
SELECT to_timestamp(
|
||||
('x' || left(replace(uuid_val::text, '-', ''), 12))::bit(48)::bigint / 1000.0
|
||||
);
|
||||
$$;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP FUNCTION IF EXISTS public.uuid_extract_timestamp(uuid);
|
||||
DROP FUNCTION IF EXISTS public.uuid_v7(timestamptz);
|
||||
|
||||
DROP SCHEMA IF EXISTS activity;
|
||||
DROP SCHEMA IF EXISTS acl;
|
||||
DROP SCHEMA IF EXISTS data;
|
||||
DROP SCHEMA IF EXISTS core;
|
||||
|
||||
DROP EXTENSION IF EXISTS "uuid-ossp";
|
||||
DROP EXTENSION IF EXISTS pgcrypto;
|
||||
36
backend/migrations/002_core_tables.sql
Normal file
36
backend/migrations/002_core_tables.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TABLE core.users (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(32) NOT NULL,
|
||||
password text NOT NULL, -- bcrypt hash via pgcrypto
|
||||
is_admin boolean NOT NULL DEFAULT false,
|
||||
can_create boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__users__name UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE core.mime_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(127) NOT NULL,
|
||||
extension varchar(16) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__mime_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE core.object_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(32) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__object_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE core.users IS 'Application users';
|
||||
COMMENT ON TABLE core.mime_types IS 'Whitelist of supported MIME types';
|
||||
COMMENT ON TABLE core.object_types IS 'Reference: entity types for ACL and audit log';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS core.object_types;
|
||||
DROP TABLE IF EXISTS core.mime_types;
|
||||
DROP TABLE IF EXISTS core.users;
|
||||
118
backend/migrations/003_data_tables.sql
Normal file
118
backend/migrations/003_data_tables.sql
Normal file
@ -0,0 +1,118 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TABLE data.categories (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
color char(6),
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__categories__name UNIQUE (name),
|
||||
CONSTRAINT chk__categories__color_hex
|
||||
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
|
||||
);
|
||||
|
||||
CREATE TABLE data.tags (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
color char(6),
|
||||
category_id uuid REFERENCES data.categories(id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL,
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__tags__name UNIQUE (name),
|
||||
CONSTRAINT chk__tags__color_hex
|
||||
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
|
||||
);
|
||||
|
||||
CREATE TABLE data.tag_rules (
|
||||
when_tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
then_tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
|
||||
PRIMARY KEY (when_tag_id, then_tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE data.files (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
original_name varchar(256), -- original filename at upload time
|
||||
mime_id smallint NOT NULL REFERENCES core.mime_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken)
|
||||
notes text,
|
||||
metadata jsonb, -- user-editable key-value data
|
||||
exif jsonb NOT NULL DEFAULT '{}'::jsonb, -- EXIF data extracted at upload (immutable)
|
||||
phash bigint, -- perceptual hash for duplicate detection (future)
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
is_deleted boolean NOT NULL DEFAULT false -- soft delete (trash)
|
||||
);
|
||||
|
||||
CREATE TABLE data.file_tag (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
|
||||
PRIMARY KEY (file_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE data.pools (
|
||||
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
|
||||
name varchar(256) NOT NULL,
|
||||
notes text,
|
||||
metadata jsonb,
|
||||
creator_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
is_public boolean NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT uni__pools__name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- `position` uses integer with gaps (e.g. 1000, 2000, 3000) to allow
|
||||
-- insertions without renumbering. Compact when gaps get too small.
|
||||
CREATE TABLE data.file_pool (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
pool_id uuid NOT NULL REFERENCES data.pools(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (file_id, pool_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE data.categories IS 'Logical grouping of tags';
|
||||
COMMENT ON TABLE data.tags IS 'File labels/tags';
|
||||
COMMENT ON TABLE data.tag_rules IS 'Auto-tagging rules: when when_tag is assigned, then_tag follows';
|
||||
COMMENT ON TABLE data.files IS 'Managed files; actual content stored on disk as {id}.{ext}';
|
||||
COMMENT ON TABLE data.file_tag IS 'Many-to-many: files <-> tags';
|
||||
COMMENT ON TABLE data.pools IS 'Ordered collections of files';
|
||||
COMMENT ON TABLE data.file_pool IS 'Many-to-many: files <-> pools, with ordering';
|
||||
|
||||
COMMENT ON COLUMN data.files.original_name IS 'Original filename at upload time';
|
||||
COMMENT ON COLUMN data.files.content_datetime IS 'Content datetime (e.g. when photo was taken); falls back to EXIF DateTimeOriginal';
|
||||
COMMENT ON COLUMN data.files.metadata IS 'User-editable key-value metadata';
|
||||
COMMENT ON COLUMN data.files.exif IS 'EXIF data extracted at upload time (immutable, system-managed)';
|
||||
COMMENT ON COLUMN data.files.phash IS 'Perceptual hash for image/video duplicate detection';
|
||||
COMMENT ON COLUMN data.files.is_deleted IS 'Soft-deleted files (trash); true = in recycle bin';
|
||||
COMMENT ON COLUMN data.file_pool.position IS 'Manual ordering within pool; uses gapped integers';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS data.file_pool;
|
||||
DROP TABLE IF EXISTS data.pools;
|
||||
DROP TABLE IF EXISTS data.file_tag;
|
||||
DROP TABLE IF EXISTS data.files;
|
||||
DROP TABLE IF EXISTS data.tag_rules;
|
||||
DROP TABLE IF EXISTS data.tags;
|
||||
DROP TABLE IF EXISTS data.categories;
|
||||
22
backend/migrations/004_acl_tables.sql
Normal file
22
backend/migrations/004_acl_tables.sql
Normal file
@ -0,0 +1,22 @@
|
||||
-- +goose Up
|
||||
|
||||
-- If is_public=true on the object, it is accessible to everyone (ACL ignored).
|
||||
-- If is_public=false, only creator and users with can_view=true see it.
|
||||
-- Admins bypass all ACL checks.
|
||||
CREATE TABLE acl.permissions (
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
object_type_id smallint NOT NULL REFERENCES core.object_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_id uuid NOT NULL,
|
||||
can_view boolean NOT NULL DEFAULT true,
|
||||
can_edit boolean NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (user_id, object_type_id, object_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE acl.permissions IS 'Per-object permissions (used when is_public=false)';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS acl.permissions;
|
||||
81
backend/migrations/005_activity_tables.sql
Normal file
81
backend/migrations/005_activity_tables.sql
Normal file
@ -0,0 +1,81 @@
|
||||
-- +goose Up
|
||||
|
||||
CREATE TABLE activity.action_types (
|
||||
id smallint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(64) NOT NULL,
|
||||
|
||||
CONSTRAINT uni__action_types__name UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE activity.sessions (
|
||||
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
token_hash text NOT NULL, -- hashed session token
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_agent varchar(256) NOT NULL,
|
||||
started_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
expires_at timestamptz,
|
||||
last_activity timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
CONSTRAINT uni__sessions__token_hash UNIQUE (token_hash)
|
||||
);
|
||||
|
||||
CREATE TABLE activity.file_views (
|
||||
file_id uuid NOT NULL REFERENCES data.files(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
viewed_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
PRIMARY KEY (file_id, viewed_at, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE activity.pool_views (
|
||||
pool_id uuid NOT NULL REFERENCES data.pools(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
viewed_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
|
||||
PRIMARY KEY (pool_id, viewed_at, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE activity.tag_uses (
|
||||
tag_id uuid NOT NULL REFERENCES data.tags(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
used_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||
is_included boolean NOT NULL, -- true=included in filter, false=excluded
|
||||
|
||||
PRIMARY KEY (tag_id, used_at, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE activity.audit_log (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id smallint NOT NULL REFERENCES core.users(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
action_type_id smallint NOT NULL REFERENCES activity.action_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_type_id smallint REFERENCES core.object_types(id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
object_id uuid,
|
||||
details jsonb, -- action-specific payload
|
||||
performed_at timestamptz NOT NULL DEFAULT statement_timestamp()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE activity.action_types IS 'Reference: types of auditable user actions';
|
||||
COMMENT ON TABLE activity.sessions IS 'Active user sessions';
|
||||
COMMENT ON TABLE activity.file_views IS 'File view history';
|
||||
COMMENT ON TABLE activity.pool_views IS 'Pool view history';
|
||||
COMMENT ON TABLE activity.tag_uses IS 'Tag usage in filters';
|
||||
COMMENT ON TABLE activity.audit_log IS 'Unified audit trail for all user actions';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS activity.audit_log;
|
||||
DROP TABLE IF EXISTS activity.tag_uses;
|
||||
DROP TABLE IF EXISTS activity.pool_views;
|
||||
DROP TABLE IF EXISTS activity.file_views;
|
||||
DROP TABLE IF EXISTS activity.sessions;
|
||||
DROP TABLE IF EXISTS activity.action_types;
|
||||
87
backend/migrations/006_indexes.sql
Normal file
87
backend/migrations/006_indexes.sql
Normal file
@ -0,0 +1,87 @@
|
||||
-- +goose Up
|
||||
|
||||
-- core
|
||||
CREATE INDEX idx__users__name ON core.users USING hash (name);
|
||||
|
||||
-- data.categories
|
||||
CREATE INDEX idx__categories__creator_id ON data.categories USING hash (creator_id);
|
||||
|
||||
-- data.tags
|
||||
CREATE INDEX idx__tags__category_id ON data.tags USING hash (category_id);
|
||||
CREATE INDEX idx__tags__creator_id ON data.tags USING hash (creator_id);
|
||||
|
||||
-- data.tag_rules
|
||||
CREATE INDEX idx__tag_rules__when ON data.tag_rules USING hash (when_tag_id);
|
||||
CREATE INDEX idx__tag_rules__then ON data.tag_rules USING hash (then_tag_id);
|
||||
|
||||
-- data.files
|
||||
CREATE INDEX idx__files__mime_id ON data.files USING hash (mime_id);
|
||||
CREATE INDEX idx__files__creator_id ON data.files USING hash (creator_id);
|
||||
CREATE INDEX idx__files__content_datetime ON data.files USING btree (content_datetime DESC NULLS LAST);
|
||||
CREATE INDEX idx__files__is_deleted ON data.files USING btree (is_deleted) WHERE is_deleted = true;
|
||||
CREATE INDEX idx__files__phash ON data.files USING btree (phash) WHERE phash IS NOT NULL;
|
||||
|
||||
-- data.file_tag
|
||||
CREATE INDEX idx__file_tag__tag_id ON data.file_tag USING hash (tag_id);
|
||||
CREATE INDEX idx__file_tag__file_id ON data.file_tag USING hash (file_id);
|
||||
|
||||
-- data.pools
|
||||
CREATE INDEX idx__pools__creator_id ON data.pools USING hash (creator_id);
|
||||
|
||||
-- data.file_pool
|
||||
CREATE INDEX idx__file_pool__pool_id ON data.file_pool USING hash (pool_id);
|
||||
CREATE INDEX idx__file_pool__file_id ON data.file_pool USING hash (file_id);
|
||||
|
||||
-- acl.permissions
|
||||
CREATE INDEX idx__acl__object ON acl.permissions USING btree (object_type_id, object_id);
|
||||
CREATE INDEX idx__acl__user ON acl.permissions USING hash (user_id);
|
||||
|
||||
-- activity.sessions
|
||||
CREATE INDEX idx__sessions__user_id ON activity.sessions USING hash (user_id);
|
||||
CREATE INDEX idx__sessions__token_hash ON activity.sessions USING hash (token_hash);
|
||||
|
||||
-- activity.file_views
|
||||
CREATE INDEX idx__file_views__user_id ON activity.file_views USING hash (user_id);
|
||||
|
||||
-- activity.pool_views
|
||||
CREATE INDEX idx__pool_views__user_id ON activity.pool_views USING hash (user_id);
|
||||
|
||||
-- activity.tag_uses
|
||||
CREATE INDEX idx__tag_uses__user_id ON activity.tag_uses USING hash (user_id);
|
||||
|
||||
-- activity.audit_log
|
||||
CREATE INDEX idx__audit_log__user_id ON activity.audit_log USING hash (user_id);
|
||||
CREATE INDEX idx__audit_log__action_type_id ON activity.audit_log USING hash (action_type_id);
|
||||
CREATE INDEX idx__audit_log__object ON activity.audit_log USING btree (object_type_id, object_id)
|
||||
WHERE object_id IS NOT NULL;
|
||||
CREATE INDEX idx__audit_log__performed_at ON activity.audit_log USING btree (performed_at DESC);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP INDEX IF EXISTS activity.idx__audit_log__performed_at;
|
||||
DROP INDEX IF EXISTS activity.idx__audit_log__object;
|
||||
DROP INDEX IF EXISTS activity.idx__audit_log__action_type_id;
|
||||
DROP INDEX IF EXISTS activity.idx__audit_log__user_id;
|
||||
DROP INDEX IF EXISTS activity.idx__tag_uses__user_id;
|
||||
DROP INDEX IF EXISTS activity.idx__pool_views__user_id;
|
||||
DROP INDEX IF EXISTS activity.idx__file_views__user_id;
|
||||
DROP INDEX IF EXISTS activity.idx__sessions__token_hash;
|
||||
DROP INDEX IF EXISTS activity.idx__sessions__user_id;
|
||||
DROP INDEX IF EXISTS acl.idx__acl__user;
|
||||
DROP INDEX IF EXISTS acl.idx__acl__object;
|
||||
DROP INDEX IF EXISTS data.idx__file_pool__file_id;
|
||||
DROP INDEX IF EXISTS data.idx__file_pool__pool_id;
|
||||
DROP INDEX IF EXISTS data.idx__pools__creator_id;
|
||||
DROP INDEX IF EXISTS data.idx__file_tag__file_id;
|
||||
DROP INDEX IF EXISTS data.idx__file_tag__tag_id;
|
||||
DROP INDEX IF EXISTS data.idx__files__phash;
|
||||
DROP INDEX IF EXISTS data.idx__files__is_deleted;
|
||||
DROP INDEX IF EXISTS data.idx__files__content_datetime;
|
||||
DROP INDEX IF EXISTS data.idx__files__creator_id;
|
||||
DROP INDEX IF EXISTS data.idx__files__mime_id;
|
||||
DROP INDEX IF EXISTS data.idx__tag_rules__then;
|
||||
DROP INDEX IF EXISTS data.idx__tag_rules__when;
|
||||
DROP INDEX IF EXISTS data.idx__tags__creator_id;
|
||||
DROP INDEX IF EXISTS data.idx__tags__category_id;
|
||||
DROP INDEX IF EXISTS data.idx__categories__creator_id;
|
||||
DROP INDEX IF EXISTS core.idx__users__name;
|
||||
32
backend/migrations/007_seed_data.sql
Normal file
32
backend/migrations/007_seed_data.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- +goose Up
|
||||
|
||||
INSERT INTO core.object_types (name) VALUES
|
||||
('file'), ('tag'), ('category'), ('pool');
|
||||
|
||||
INSERT INTO activity.action_types (name) VALUES
|
||||
-- Auth
|
||||
('user_login'), ('user_logout'),
|
||||
-- Files
|
||||
('file_create'), ('file_edit'), ('file_delete'), ('file_restore'),
|
||||
('file_permanent_delete'), ('file_replace'),
|
||||
-- Tags
|
||||
('tag_create'), ('tag_edit'), ('tag_delete'),
|
||||
-- Categories
|
||||
('category_create'), ('category_edit'), ('category_delete'),
|
||||
-- Pools
|
||||
('pool_create'), ('pool_edit'), ('pool_delete'),
|
||||
-- Relations
|
||||
('file_tag_add'), ('file_tag_remove'),
|
||||
('file_pool_add'), ('file_pool_remove'),
|
||||
-- ACL
|
||||
('acl_change'),
|
||||
-- Admin
|
||||
('user_create'), ('user_delete'), ('user_block'), ('user_unblock'),
|
||||
('user_role_change'),
|
||||
-- Sessions
|
||||
('session_terminate');
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DELETE FROM activity.action_types;
|
||||
DELETE FROM core.object_types;
|
||||
Loading…
x
Reference in New Issue
Block a user