CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS files_nodes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), parent_id UUID REFERENCES files_nodes(id) ON DELETE CASCADE, node_type TEXT NOT NULL CHECK (node_type IN ('folder', 'file', 'google_sheet', 'office_document')), title TEXT NOT NULL, owner_user_id UUID NOT NULL, owner_department_id UUID, created_by UUID NOT NULL, updated_by UUID, storage_key TEXT, original_filename TEXT, mime_type TEXT, extension TEXT, size_bytes BIGINT NOT NULL DEFAULT 0, office_format TEXT, external_url TEXT, version INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, CONSTRAINT files_nodes_file_storage_check CHECK ( node_type IN ('folder', 'google_sheet') OR storage_key IS NOT NULL ) ); CREATE INDEX IF NOT EXISTS files_nodes_parent_idx ON files_nodes(parent_id) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS files_nodes_owner_idx ON files_nodes(owner_user_id) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS files_nodes_type_idx ON files_nodes(node_type) WHERE deleted_at IS NULL; CREATE TABLE IF NOT EXISTS files_access ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), node_id UUID NOT NULL REFERENCES files_nodes(id) ON DELETE CASCADE, user_id UUID NOT NULL, access_level TEXT NOT NULL CHECK (access_level IN ('view', 'edit')), granted_by UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (node_id, user_id) ); CREATE INDEX IF NOT EXISTS files_access_node_idx ON files_access(node_id); CREATE INDEX IF NOT EXISTS files_access_user_idx ON files_access(user_id); CREATE TABLE IF NOT EXISTS files_public_links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), node_id UUID NOT NULL REFERENCES files_nodes(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, access_level TEXT NOT NULL DEFAULT 'view' CHECK (access_level = 'view'), expires_at TIMESTAMPTZ NOT NULL, created_by UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), revoked_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS files_public_links_node_idx ON files_public_links(node_id); CREATE INDEX IF NOT EXISTS files_public_links_active_idx ON files_public_links(token_hash, expires_at) WHERE revoked_at IS NULL; CREATE TABLE IF NOT EXISTS files_audit_events ( id BIGSERIAL PRIMARY KEY, actor_user_id UUID, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id UUID, meta JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS files_audit_events_actor_idx ON files_audit_events(actor_user_id, created_at DESC); CREATE INDEX IF NOT EXISTS files_audit_events_entity_idx ON files_audit_events(entity_type, entity_id, created_at DESC);