Initial commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
DROP TRIGGER IF EXISTS trg_jobs_updated_at ON jobs;
|
||||
DROP FUNCTION IF EXISTS touch_jobs_updated_at();
|
||||
|
||||
DROP TABLE IF EXISTS webauthn_challenges;
|
||||
DROP TABLE IF EXISTS user_passkeys;
|
||||
DROP TABLE IF EXISTS webdav_tokens;
|
||||
ALTER TABLE documents DROP CONSTRAINT IF EXISTS documents_current_version_fk;
|
||||
DROP TABLE IF EXISTS document_asset_objects;
|
||||
DROP TABLE IF EXISTS document_assets;
|
||||
DROP TABLE IF EXISTS document_versions;
|
||||
DROP TABLE IF EXISTS document_tags;
|
||||
DROP TABLE IF EXISTS document_correspondents;
|
||||
DROP TABLE IF EXISTS correspondents;
|
||||
DROP TABLE IF EXISTS documents;
|
||||
DROP TABLE IF EXISTS folders;
|
||||
DROP TABLE IF EXISTS tags;
|
||||
DROP TABLE IF EXISTS jobs;
|
||||
DROP TABLE IF EXISTS refresh_tokens;
|
||||
DROP TABLE IF EXISTS user_memberships;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS tenants;
|
||||
|
||||
DROP TYPE IF EXISTS tenant_status;
|
||||
|
||||
DROP EXTENSION IF EXISTS "pgcrypto";
|
||||
@@ -0,0 +1,295 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE TYPE tenant_status AS ENUM ('creating', 'active', 'suspended', 'deleting', 'error');
|
||||
|
||||
CREATE TABLE tenants (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
storage_root TEXT,
|
||||
quickwit_index TEXT,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status tenant_status NOT NULL,
|
||||
created_by UUID
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX tenants_storage_root_unique
|
||||
ON tenants (storage_root)
|
||||
WHERE storage_root IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX tenants_quickwit_index_unique
|
||||
ON tenants (quickwit_index)
|
||||
WHERE quickwit_index IS NOT NULL;
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE user_memberships (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (user_id, tenant_id)
|
||||
);
|
||||
|
||||
CREATE INDEX user_memberships_tenant_id_idx ON user_memberships(tenant_id);
|
||||
CREATE INDEX user_memberships_user_id_idx ON user_memberships(user_id);
|
||||
|
||||
CREATE TABLE folders (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
parent_id UUID REFERENCES folders(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_folders_parent ON folders(parent_id);
|
||||
CREATE INDEX folders_tenant_id_idx ON folders(tenant_id);
|
||||
CREATE UNIQUE INDEX folders_tenant_parent_name_unique_idx
|
||||
ON folders (
|
||||
tenant_id,
|
||||
COALESCE(parent_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
name
|
||||
);
|
||||
|
||||
CREATE TABLE documents (
|
||||
id UUID PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
content_type VARCHAR(100),
|
||||
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
issued_at TIMESTAMPTZ,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
current_version_id UUID NOT NULL,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_documents_folder ON documents(folder_id);
|
||||
CREATE INDEX idx_documents_deleted_at ON documents(deleted_at);
|
||||
CREATE INDEX documents_tenant_id_idx ON documents(tenant_id);
|
||||
CREATE INDEX idx_documents_current_version_id ON documents(current_version_id);
|
||||
CREATE INDEX idx_documents_folder_title
|
||||
ON documents (
|
||||
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
title
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX documents_tenant_folder_filename_unique
|
||||
ON documents (
|
||||
tenant_id,
|
||||
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
filename
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE document_versions (
|
||||
id UUID PRIMARY KEY,
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
version_number INT NOT NULL,
|
||||
s3_key VARCHAR(500) NOT NULL,
|
||||
size_bytes BIGINT NOT NULL,
|
||||
checksum VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
CONSTRAINT document_versions_unique_version UNIQUE (document_id, version_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_versions_document ON document_versions(document_id);
|
||||
CREATE INDEX document_versions_tenant_id_idx ON document_versions(tenant_id);
|
||||
|
||||
ALTER TABLE documents
|
||||
ADD CONSTRAINT documents_current_version_fk
|
||||
FOREIGN KEY (current_version_id)
|
||||
REFERENCES document_versions(id)
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
CREATE TABLE tags (
|
||||
id UUID PRIMARY KEY,
|
||||
label VARCHAR(100) NOT NULL,
|
||||
color VARCHAR(7),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX tags_tenant_label_unique ON tags(tenant_id, label);
|
||||
CREATE INDEX tags_tenant_id_idx ON tags(tenant_id);
|
||||
|
||||
CREATE TABLE document_tags (
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
assigned_by UUID REFERENCES users(id),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
PRIMARY KEY (document_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_tags_tag ON document_tags(tag_id);
|
||||
CREATE INDEX document_tags_tenant_id_idx ON document_tags(tenant_id);
|
||||
|
||||
CREATE TABLE correspondents (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX correspondents_tenant_name_unique
|
||||
ON correspondents (tenant_id, name);
|
||||
CREATE INDEX correspondents_tenant_id_idx ON correspondents(tenant_id);
|
||||
|
||||
CREATE TABLE document_correspondents (
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
correspondent_id UUID NOT NULL REFERENCES correspondents(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
assigned_by UUID REFERENCES users(id),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
PRIMARY KEY (document_id, correspondent_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_correspondents_document ON document_correspondents(document_id);
|
||||
CREATE INDEX idx_document_correspondents_correspondent ON document_correspondents(correspondent_id);
|
||||
CREATE INDEX document_correspondents_tenant_id_idx ON document_correspondents(tenant_id);
|
||||
|
||||
CREATE TABLE document_assets (
|
||||
id UUID PRIMARY KEY,
|
||||
document_version_id UUID NOT NULL REFERENCES document_versions(id) ON DELETE CASCADE,
|
||||
asset_type TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
cardinality INTEGER,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
CONSTRAINT document_assets_unique UNIQUE (document_version_id, asset_type),
|
||||
CONSTRAINT document_assets_cardinality_positive CHECK (cardinality IS NULL OR cardinality >= 1)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_assets_version ON document_assets(document_version_id);
|
||||
CREATE INDEX idx_document_assets_type ON document_assets(asset_type);
|
||||
CREATE INDEX document_assets_tenant_id_idx ON document_assets(tenant_id);
|
||||
|
||||
CREATE TABLE document_asset_objects (
|
||||
id UUID PRIMARY KEY,
|
||||
asset_id UUID NOT NULL REFERENCES document_assets(id) ON DELETE CASCADE,
|
||||
ordinal INTEGER NOT NULL,
|
||||
s3_key TEXT NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
CONSTRAINT document_asset_objects_ordinal_positive CHECK (ordinal >= 1),
|
||||
CONSTRAINT document_asset_objects_asset_ordinal_unique UNIQUE (asset_id, ordinal)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_asset_objects_asset_ordinal
|
||||
ON document_asset_objects(asset_id, ordinal);
|
||||
CREATE INDEX document_asset_objects_tenant_id_idx ON document_asset_objects(tenant_id);
|
||||
|
||||
CREATE TABLE jobs (
|
||||
id UUID PRIMARY KEY,
|
||||
job_type TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'queued',
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
run_after TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
CONSTRAINT jobs_status_check CHECK (status IN ('queued', 'processing', 'succeeded', 'failed'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_jobs_status_run_after ON jobs(status, run_after);
|
||||
CREATE INDEX idx_jobs_job_type ON jobs(job_type);
|
||||
CREATE INDEX jobs_tenant_id_idx ON jobs(tenant_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION touch_jobs_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_jobs_updated_at
|
||||
BEFORE UPDATE ON jobs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION touch_jobs_updated_at();
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL,
|
||||
issued_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
|
||||
CREATE INDEX refresh_tokens_tenant_id_idx ON refresh_tokens(tenant_id);
|
||||
|
||||
CREATE TABLE webdav_tokens (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
token_prefix TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
label TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX webdav_tokens_token_prefix_key ON webdav_tokens(token_prefix);
|
||||
CREATE INDEX webdav_tokens_user_tenant_idx ON webdav_tokens(user_id, tenant_id);
|
||||
|
||||
CREATE TABLE user_passkeys (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
credential_id BYTEA NOT NULL UNIQUE,
|
||||
public_key BYTEA NOT NULL,
|
||||
credential JSONB NOT NULL,
|
||||
sign_count BIGINT NOT NULL,
|
||||
transports TEXT[] NOT NULL DEFAULT '{}'::text[],
|
||||
aaguid UUID,
|
||||
nickname TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by UUID,
|
||||
revoked_reason TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX user_passkeys_user_id_idx ON user_passkeys(user_id);
|
||||
|
||||
CREATE TABLE webauthn_challenges (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
purpose TEXT NOT NULL,
|
||||
challenge BYTEA NOT NULL,
|
||||
state BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT webauthn_challenges_purpose_check CHECK (purpose IN ('registration', 'authentication'))
|
||||
);
|
||||
|
||||
CREATE INDEX webauthn_challenges_user_id_idx ON webauthn_challenges(user_id);
|
||||
CREATE INDEX webauthn_challenges_expires_at_idx ON webauthn_challenges(expires_at);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE documents
|
||||
RENAME COLUMN created_at TO uploaded_at;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE documents
|
||||
RENAME COLUMN uploaded_at TO created_at;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE magic_tokens;
|
||||
DROP TYPE magic_token_kind;
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TYPE magic_token_kind AS ENUM ('email_login', 'demo_login');
|
||||
|
||||
CREATE TABLE magic_tokens (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
kind magic_token_kind NOT NULL,
|
||||
token_hash VARCHAR NOT NULL UNIQUE,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
max_uses INTEGER,
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
last_used_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX magic_tokens_token_hash_idx ON magic_tokens (token_hash);
|
||||
CREATE INDEX magic_tokens_expires_at_idx ON magic_tokens (expires_at);
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Move tables and types back to the public schema
|
||||
ALTER TABLE tenant.webdav_tokens SET SCHEMA public;
|
||||
ALTER TABLE tenant.user_memberships SET SCHEMA public;
|
||||
ALTER TABLE tenant.refresh_tokens SET SCHEMA public;
|
||||
ALTER TABLE tenant.tags SET SCHEMA public;
|
||||
ALTER TABLE tenant.document_correspondents SET SCHEMA public;
|
||||
ALTER TABLE tenant.document_tags SET SCHEMA public;
|
||||
ALTER TABLE tenant.document_asset_objects SET SCHEMA public;
|
||||
ALTER TABLE tenant.document_assets SET SCHEMA public;
|
||||
ALTER TABLE tenant.document_versions SET SCHEMA public;
|
||||
ALTER TABLE tenant.documents SET SCHEMA public;
|
||||
ALTER TABLE tenant.folders SET SCHEMA public;
|
||||
ALTER TABLE tenant.correspondents SET SCHEMA public;
|
||||
|
||||
ALTER FUNCTION shared.touch_jobs_updated_at() SET SCHEMA public;
|
||||
|
||||
ALTER TABLE shared.magic_tokens SET SCHEMA public;
|
||||
ALTER TABLE shared.jobs SET SCHEMA public;
|
||||
ALTER TABLE shared.webauthn_challenges SET SCHEMA public;
|
||||
ALTER TABLE shared.user_passkeys SET SCHEMA public;
|
||||
ALTER TABLE shared.users SET SCHEMA public;
|
||||
ALTER TABLE shared.tenants SET SCHEMA public;
|
||||
|
||||
ALTER TYPE shared.magic_token_kind SET SCHEMA public;
|
||||
ALTER TYPE shared.tenant_status SET SCHEMA public;
|
||||
|
||||
DROP SCHEMA IF EXISTS tenant CASCADE;
|
||||
DROP SCHEMA IF EXISTS shared CASCADE;
|
||||
@@ -0,0 +1,45 @@
|
||||
CREATE SCHEMA IF NOT EXISTS shared;
|
||||
CREATE SCHEMA IF NOT EXISTS tenant;
|
||||
|
||||
-- Move global types and tables into the shared schema
|
||||
ALTER TYPE tenant_status SET SCHEMA shared;
|
||||
ALTER TYPE magic_token_kind SET SCHEMA shared;
|
||||
|
||||
ALTER TABLE tenants SET SCHEMA shared;
|
||||
ALTER TABLE users SET SCHEMA shared;
|
||||
ALTER TABLE user_passkeys SET SCHEMA shared;
|
||||
ALTER TABLE webauthn_challenges SET SCHEMA shared;
|
||||
ALTER TABLE jobs SET SCHEMA shared;
|
||||
ALTER TABLE magic_tokens SET SCHEMA shared;
|
||||
|
||||
ALTER FUNCTION touch_jobs_updated_at() SET SCHEMA shared;
|
||||
|
||||
-- Move tenant-scoped tables into the tenant schema
|
||||
ALTER TABLE correspondents SET SCHEMA tenant;
|
||||
ALTER TABLE folders SET SCHEMA tenant;
|
||||
ALTER TABLE documents SET SCHEMA tenant;
|
||||
ALTER TABLE document_versions SET SCHEMA tenant;
|
||||
ALTER TABLE document_assets SET SCHEMA tenant;
|
||||
ALTER TABLE document_asset_objects SET SCHEMA tenant;
|
||||
ALTER TABLE document_tags SET SCHEMA tenant;
|
||||
ALTER TABLE document_correspondents SET SCHEMA tenant;
|
||||
ALTER TABLE tags SET SCHEMA tenant;
|
||||
ALTER TABLE refresh_tokens SET SCHEMA tenant;
|
||||
ALTER TABLE user_memberships SET SCHEMA tenant;
|
||||
ALTER TABLE webdav_tokens SET SCHEMA tenant;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'papercrate_app') THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
GRANT USAGE ON SCHEMA shared TO papercrate_app;
|
||||
GRANT USAGE ON SCHEMA tenant TO papercrate_app;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA shared TO papercrate_app;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tenant TO papercrate_app;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA shared GRANT SELECT ON TABLES TO papercrate_app;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA tenant GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO papercrate_app;
|
||||
END
|
||||
$$;
|
||||
@@ -0,0 +1,51 @@
|
||||
DROP POLICY IF EXISTS tenant_membership_select_policy ON tenant.user_memberships;
|
||||
ALTER TABLE tenant.user_memberships NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.user_memberships DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.webdav_tokens;
|
||||
ALTER TABLE tenant.webdav_tokens NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.webdav_tokens DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.tags;
|
||||
ALTER TABLE tenant.tags NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.tags DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_refresh_token_policy ON tenant.refresh_tokens;
|
||||
ALTER TABLE tenant.refresh_tokens NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.refresh_tokens DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.folders;
|
||||
ALTER TABLE tenant.folders NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.folders DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.documents;
|
||||
ALTER TABLE tenant.documents NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.documents DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.document_versions;
|
||||
ALTER TABLE tenant.document_versions NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.document_versions DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.document_tags;
|
||||
ALTER TABLE tenant.document_tags NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.document_tags DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.document_correspondents;
|
||||
ALTER TABLE tenant.document_correspondents NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.document_correspondents DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.document_assets;
|
||||
ALTER TABLE tenant.document_assets NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.document_assets DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.document_asset_objects;
|
||||
ALTER TABLE tenant.document_asset_objects NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.document_asset_objects DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_isolation_policy ON tenant.correspondents;
|
||||
ALTER TABLE tenant.correspondents NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.correspondents DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP FUNCTION IF EXISTS shared.current_refresh_token_hash();
|
||||
DROP FUNCTION IF EXISTS shared.current_user_id();
|
||||
DROP FUNCTION IF EXISTS shared.current_tenant_id();
|
||||
@@ -0,0 +1,69 @@
|
||||
CREATE OR REPLACE FUNCTION shared.current_tenant_id() RETURNS uuid AS $$
|
||||
SELECT CASE
|
||||
WHEN setting IS NULL OR setting = '' THEN NULL
|
||||
ELSE setting::uuid
|
||||
END
|
||||
FROM (SELECT current_setting('papercrate.tenant_id', true) AS setting) s;
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shared.current_user_id() RETURNS uuid AS $$
|
||||
SELECT CASE
|
||||
WHEN setting IS NULL OR setting = '' THEN NULL
|
||||
ELSE setting::uuid
|
||||
END
|
||||
FROM (SELECT current_setting('papercrate.user_id', true) AS setting) s;
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shared.current_refresh_token_hash() RETURNS text AS $$
|
||||
SELECT NULLIF(current_setting('papercrate.refresh_token_hash', true), '')
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
-- Helper to create tenant isolation policy
|
||||
CREATE OR REPLACE FUNCTION shared.ensure_tenant_policy(table_reg regclass) RETURNS void AS $$
|
||||
BEGIN
|
||||
EXECUTE format('ALTER TABLE %s ENABLE ROW LEVEL SECURITY', table_reg);
|
||||
EXECUTE format('ALTER TABLE %s FORCE ROW LEVEL SECURITY', table_reg);
|
||||
EXECUTE format(
|
||||
'CREATE POLICY tenant_isolation_policy ON %s USING (tenant_id = shared.current_tenant_id()) WITH CHECK (tenant_id = shared.current_tenant_id())',
|
||||
table_reg
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
SELECT shared.ensure_tenant_policy('tenant.correspondents');
|
||||
SELECT shared.ensure_tenant_policy('tenant.document_asset_objects');
|
||||
SELECT shared.ensure_tenant_policy('tenant.document_assets');
|
||||
SELECT shared.ensure_tenant_policy('tenant.document_correspondents');
|
||||
SELECT shared.ensure_tenant_policy('tenant.document_tags');
|
||||
SELECT shared.ensure_tenant_policy('tenant.document_versions');
|
||||
SELECT shared.ensure_tenant_policy('tenant.documents');
|
||||
SELECT shared.ensure_tenant_policy('tenant.folders');
|
||||
SELECT shared.ensure_tenant_policy('tenant.tags');
|
||||
SELECT shared.ensure_tenant_policy('tenant.webdav_tokens');
|
||||
|
||||
-- user_memberships has a special read policy to allow tenant discovery during login
|
||||
ALTER TABLE tenant.user_memberships ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.user_memberships FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_membership_select_policy ON tenant.user_memberships
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_user_id() IS NOT NULL
|
||||
AND user_id = shared.current_user_id()
|
||||
)
|
||||
)
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
|
||||
ALTER TABLE tenant.refresh_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.refresh_tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_refresh_token_policy ON tenant.refresh_tokens
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_refresh_token_hash() IS NOT NULL
|
||||
AND token_hash = shared.current_refresh_token_hash()
|
||||
)
|
||||
)
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
|
||||
DROP FUNCTION shared.ensure_tenant_policy(regclass);
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP POLICY IF EXISTS tenant_webdav_token_policy ON tenant.webdav_tokens;
|
||||
ALTER TABLE tenant.webdav_tokens NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.webdav_tokens DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP FUNCTION IF EXISTS shared.current_webdav_token_prefix();
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE OR REPLACE FUNCTION shared.current_webdav_token_prefix() RETURNS text AS $$
|
||||
SELECT NULLIF(current_setting('papercrate.webdav_token_prefix', true), '')
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
ALTER TABLE tenant.webdav_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.webdav_tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_webdav_token_policy ON tenant.webdav_tokens
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_webdav_token_prefix() IS NOT NULL
|
||||
AND token_prefix = shared.current_webdav_token_prefix()
|
||||
)
|
||||
)
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
@@ -0,0 +1,31 @@
|
||||
DROP POLICY IF EXISTS tenant_api_token_policy ON tenant.api_tokens;
|
||||
DROP FUNCTION IF EXISTS shared.current_api_token_prefix();
|
||||
|
||||
ALTER TABLE tenant.api_tokens DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.api_tokens NO FORCE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
DROP COLUMN IF EXISTS capabilities;
|
||||
|
||||
DROP TYPE IF EXISTS shared.api_token_capability;
|
||||
|
||||
ALTER TABLE tenant.api_tokens RENAME TO webdav_tokens;
|
||||
ALTER INDEX tenant.api_tokens_token_prefix_key RENAME TO webdav_tokens_token_prefix_key;
|
||||
ALTER INDEX tenant.api_tokens_user_tenant_idx RENAME TO webdav_tokens_user_tenant_idx;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shared.current_webdav_token_prefix() RETURNS text AS $$
|
||||
SELECT NULLIF(current_setting('papercrate.webdav_token_prefix', true), '')
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
ALTER TABLE tenant.webdav_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.webdav_tokens FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY tenant_webdav_token_policy ON tenant.webdav_tokens
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_webdav_token_prefix() IS NOT NULL
|
||||
AND token_prefix = shared.current_webdav_token_prefix()
|
||||
)
|
||||
)
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
@@ -0,0 +1,28 @@
|
||||
ALTER TABLE tenant.webdav_tokens RENAME TO api_tokens;
|
||||
ALTER INDEX tenant.webdav_tokens_token_prefix_key RENAME TO api_tokens_token_prefix_key;
|
||||
ALTER INDEX tenant.webdav_tokens_user_tenant_idx RENAME TO api_tokens_user_tenant_idx;
|
||||
|
||||
DROP POLICY IF EXISTS tenant_webdav_token_policy ON tenant.api_tokens;
|
||||
DROP FUNCTION IF EXISTS shared.current_webdav_token_prefix();
|
||||
|
||||
CREATE TYPE shared.api_token_capability AS ENUM ('api', 'webdav');
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
ADD COLUMN capabilities shared.api_token_capability[] NOT NULL DEFAULT ARRAY['webdav']::shared.api_token_capability[];
|
||||
|
||||
ALTER TABLE tenant.api_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tenant.api_tokens FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shared.current_api_token_prefix() RETURNS text AS $$
|
||||
SELECT NULLIF(current_setting('papercrate.api_token_prefix', true), '')
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
CREATE POLICY tenant_api_token_policy ON tenant.api_tokens
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_api_token_prefix() IS NOT NULL
|
||||
AND token_prefix = shared.current_api_token_prefix()
|
||||
)
|
||||
)
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE OR REPLACE FUNCTION shared.current_refresh_token_hash() RETURNS text AS $$
|
||||
SELECT NULLIF(current_setting('papercrate.refresh_token_hash', true), '')
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_refresh_token_hash() IS NOT NULL
|
||||
AND token_hash = shared.current_refresh_token_hash()
|
||||
)
|
||||
);
|
||||
|
||||
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
|
||||
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
|
||||
RENAME TO tenant_refresh_token_policy;
|
||||
|
||||
ALTER INDEX tenant.idx_user_sessions_user_id RENAME TO idx_refresh_tokens_user_id;
|
||||
ALTER INDEX tenant.idx_user_sessions_token_hash RENAME TO idx_refresh_tokens_token_hash;
|
||||
ALTER INDEX tenant.user_sessions_tenant_id_idx RENAME TO refresh_tokens_tenant_id_idx;
|
||||
|
||||
ALTER TABLE tenant.user_sessions RENAME TO refresh_tokens;
|
||||
|
||||
DROP FUNCTION IF EXISTS shared.current_user_session_hash();
|
||||
@@ -0,0 +1,26 @@
|
||||
ALTER TABLE tenant.refresh_tokens RENAME TO user_sessions;
|
||||
|
||||
ALTER INDEX tenant.idx_refresh_tokens_user_id RENAME TO idx_user_sessions_user_id;
|
||||
ALTER INDEX tenant.idx_refresh_tokens_token_hash RENAME TO idx_user_sessions_token_hash;
|
||||
ALTER INDEX tenant.refresh_tokens_tenant_id_idx RENAME TO user_sessions_tenant_id_idx;
|
||||
|
||||
ALTER POLICY tenant_refresh_token_policy ON tenant.user_sessions
|
||||
RENAME TO tenant_user_session_policy;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shared.current_user_session_hash() RETURNS text AS $$
|
||||
SELECT NULLIF(current_setting('papercrate.user_session_hash', true), '')
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
||||
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
|
||||
USING (
|
||||
tenant_id = shared.current_tenant_id()
|
||||
OR (
|
||||
shared.current_user_session_hash() IS NOT NULL
|
||||
AND token_hash = shared.current_user_session_hash()
|
||||
)
|
||||
);
|
||||
|
||||
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
|
||||
WITH CHECK (tenant_id = shared.current_tenant_id());
|
||||
|
||||
DROP FUNCTION IF EXISTS shared.current_refresh_token_hash();
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE tenant.document_tags
|
||||
DROP CONSTRAINT IF EXISTS document_tags_assigned_by_fkey,
|
||||
ADD CONSTRAINT document_tags_assigned_by_fkey
|
||||
FOREIGN KEY (assigned_by)
|
||||
REFERENCES shared.users (id)
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
ALTER TABLE tenant.document_correspondents
|
||||
DROP CONSTRAINT IF EXISTS document_correspondents_assigned_by_fkey,
|
||||
ADD CONSTRAINT document_correspondents_assigned_by_fkey
|
||||
FOREIGN KEY (assigned_by)
|
||||
REFERENCES shared.users (id)
|
||||
ON DELETE NO ACTION;
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE tenant.document_tags
|
||||
DROP CONSTRAINT IF EXISTS document_tags_assigned_by_fkey,
|
||||
ADD CONSTRAINT document_tags_assigned_by_fkey
|
||||
FOREIGN KEY (assigned_by)
|
||||
REFERENCES shared.users (id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE tenant.document_correspondents
|
||||
DROP CONSTRAINT IF EXISTS document_correspondents_assigned_by_fkey,
|
||||
ADD CONSTRAINT document_correspondents_assigned_by_fkey
|
||||
FOREIGN KEY (assigned_by)
|
||||
REFERENCES shared.users (id)
|
||||
ON DELETE SET NULL;
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TYPE api_token_capability AS ENUM ('api', 'webdav');
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
ADD COLUMN capabilities api_token_capability[] NOT NULL DEFAULT ARRAY[]::api_token_capability[];
|
||||
|
||||
UPDATE tenant.api_tokens t
|
||||
SET capabilities = ARRAY['api']::api_token_capability[]
|
||||
FROM tenant.capability_sets cs
|
||||
WHERE t.capability_set_id = cs.id
|
||||
AND cs.slug = 'owner';
|
||||
|
||||
UPDATE tenant.api_tokens t
|
||||
SET capabilities = ARRAY['webdav']::api_token_capability[]
|
||||
FROM tenant.capability_sets cs
|
||||
WHERE t.capability_set_id = cs.id
|
||||
AND cs.slug = 'webdav'
|
||||
AND (t.capabilities IS NULL OR array_length(t.capabilities, 1) = 0);
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
DROP COLUMN capability_set_id;
|
||||
|
||||
ALTER TABLE tenant.user_memberships
|
||||
DROP COLUMN capability_set_id;
|
||||
|
||||
DROP TABLE IF EXISTS tenant.capability_set_capabilities;
|
||||
DROP TABLE IF EXISTS tenant.capability_sets;
|
||||
|
||||
DROP TYPE IF EXISTS api_capability;
|
||||
@@ -0,0 +1,158 @@
|
||||
CREATE TYPE api_capability AS ENUM (
|
||||
'documents:read',
|
||||
'documents:edit',
|
||||
'documents:write',
|
||||
'documents:upload',
|
||||
'folders:read',
|
||||
'folders:edit',
|
||||
'folders:write',
|
||||
'tags:read',
|
||||
'tags:edit',
|
||||
'tags:write',
|
||||
'correspondents:read',
|
||||
'correspondents:edit',
|
||||
'correspondents:write',
|
||||
'profile:read',
|
||||
'profile:write',
|
||||
'webdav:read',
|
||||
'webdav:write',
|
||||
'capability_sets:read',
|
||||
'capability_sets:write'
|
||||
);
|
||||
|
||||
CREATE TABLE tenant.capability_sets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES shared.tenants(id) ON DELETE CASCADE,
|
||||
slug TEXT NOT NULL,
|
||||
cap_version INT NOT NULL DEFAULT 1,
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (tenant_id, slug)
|
||||
);
|
||||
|
||||
CREATE TABLE tenant.capability_set_capabilities (
|
||||
capability_set_id UUID NOT NULL REFERENCES tenant.capability_sets(id) ON DELETE CASCADE,
|
||||
capability api_capability NOT NULL,
|
||||
PRIMARY KEY (capability_set_id, capability)
|
||||
);
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
ADD COLUMN capability_set_id UUID REFERENCES tenant.capability_sets(id);
|
||||
|
||||
ALTER TABLE tenant.user_memberships
|
||||
ADD COLUMN capability_set_id UUID REFERENCES tenant.capability_sets(id);
|
||||
|
||||
WITH owner_sets AS (
|
||||
INSERT INTO tenant.capability_sets (tenant_id, slug, is_system)
|
||||
SELECT id, 'owner', TRUE
|
||||
FROM shared.tenants
|
||||
RETURNING id, tenant_id
|
||||
),
|
||||
user_sets AS (
|
||||
INSERT INTO tenant.capability_sets (tenant_id, slug, is_system)
|
||||
SELECT id, 'user', TRUE
|
||||
FROM shared.tenants
|
||||
RETURNING id, tenant_id
|
||||
),
|
||||
readonly_sets AS (
|
||||
INSERT INTO tenant.capability_sets (tenant_id, slug, is_system)
|
||||
SELECT id, 'readonly', TRUE
|
||||
FROM shared.tenants
|
||||
RETURNING id, tenant_id
|
||||
),
|
||||
webdav_sets AS (
|
||||
INSERT INTO tenant.capability_sets (tenant_id, slug, is_system)
|
||||
SELECT id, 'webdav', TRUE
|
||||
FROM shared.tenants
|
||||
RETURNING id, tenant_id
|
||||
)
|
||||
INSERT INTO tenant.capability_set_capabilities (capability_set_id, capability)
|
||||
SELECT set_id,
|
||||
capability
|
||||
FROM (
|
||||
SELECT os.id AS set_id,
|
||||
UNNEST(ARRAY[
|
||||
'documents:read'::api_capability,
|
||||
'documents:edit'::api_capability,
|
||||
'documents:write'::api_capability,
|
||||
'documents:upload'::api_capability,
|
||||
'folders:read'::api_capability,
|
||||
'folders:edit'::api_capability,
|
||||
'folders:write'::api_capability,
|
||||
'tags:read'::api_capability,
|
||||
'tags:edit'::api_capability,
|
||||
'tags:write'::api_capability,
|
||||
'correspondents:read'::api_capability,
|
||||
'correspondents:edit'::api_capability,
|
||||
'correspondents:write'::api_capability,
|
||||
'profile:read'::api_capability,
|
||||
'profile:write'::api_capability,
|
||||
'webdav:read'::api_capability,
|
||||
'webdav:write'::api_capability,
|
||||
'capability_sets:read'::api_capability,
|
||||
'capability_sets:write'::api_capability
|
||||
]) AS capability
|
||||
FROM owner_sets os
|
||||
UNION ALL
|
||||
SELECT us.id,
|
||||
UNNEST(ARRAY[
|
||||
'documents:read'::api_capability,
|
||||
'documents:edit'::api_capability,
|
||||
'documents:write'::api_capability,
|
||||
'documents:upload'::api_capability,
|
||||
'folders:read'::api_capability,
|
||||
'folders:edit'::api_capability,
|
||||
'folders:write'::api_capability,
|
||||
'tags:read'::api_capability,
|
||||
'tags:edit'::api_capability,
|
||||
'tags:write'::api_capability,
|
||||
'correspondents:read'::api_capability,
|
||||
'correspondents:edit'::api_capability,
|
||||
'correspondents:write'::api_capability,
|
||||
'profile:read'::api_capability,
|
||||
'profile:write'::api_capability
|
||||
]) AS capability
|
||||
FROM user_sets us
|
||||
UNION ALL
|
||||
SELECT rs.id,
|
||||
UNNEST(ARRAY[
|
||||
'documents:read'::api_capability,
|
||||
'folders:read'::api_capability,
|
||||
'tags:read'::api_capability,
|
||||
'correspondents:read'::api_capability,
|
||||
'webdav:read'::api_capability
|
||||
]) AS capability
|
||||
FROM readonly_sets rs
|
||||
UNION ALL
|
||||
SELECT ws.id,
|
||||
UNNEST(ARRAY['webdav:read'::api_capability]) AS capability
|
||||
FROM webdav_sets ws
|
||||
) seeded;
|
||||
|
||||
UPDATE tenant.user_memberships um
|
||||
SET capability_set_id = cs.id
|
||||
FROM tenant.capability_sets cs
|
||||
WHERE cs.tenant_id = um.tenant_id
|
||||
AND cs.slug = 'owner';
|
||||
|
||||
UPDATE tenant.api_tokens t
|
||||
SET capability_set_id = cs.id
|
||||
FROM tenant.capability_sets cs
|
||||
WHERE cs.tenant_id = t.tenant_id
|
||||
AND cs.slug = 'owner';
|
||||
|
||||
UPDATE tenant.api_tokens t
|
||||
SET capability_set_id = cs.id
|
||||
FROM tenant.capability_sets cs
|
||||
WHERE cs.tenant_id = t.tenant_id
|
||||
AND cs.slug = 'webdav'
|
||||
AND t.capability_set_id IS NULL;
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
ALTER COLUMN capability_set_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE tenant.api_tokens
|
||||
DROP COLUMN capabilities;
|
||||
|
||||
DROP TYPE IF EXISTS api_token_capability;
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS shared.jobs_purge_document_pending_unique;
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE UNIQUE INDEX jobs_purge_document_pending_unique
|
||||
ON shared.jobs (
|
||||
tenant_id,
|
||||
((payload ->> 'document_id')::uuid)
|
||||
)
|
||||
WHERE job_type = 'purge-document'
|
||||
AND payload ? 'document_id'
|
||||
AND status IN ('queued', 'processing');
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP INDEX IF EXISTS tenant.idx_documents_tenant_folder_updated_at;
|
||||
DROP INDEX IF EXISTS tenant.idx_documents_tenant_folder_created_at;
|
||||
DROP INDEX IF EXISTS tenant.idx_documents_tenant_folder_issued_at;
|
||||
DROP INDEX IF EXISTS tenant.idx_documents_tenant_folder_title_order;
|
||||
DROP COLLATION IF EXISTS unicode_ci;
|
||||
@@ -0,0 +1,37 @@
|
||||
CREATE COLLATION IF NOT EXISTS unicode_ci
|
||||
(provider = icu, locale = 'und-u-ks-level2');
|
||||
|
||||
CREATE INDEX idx_documents_tenant_folder_title_order
|
||||
ON tenant.documents (
|
||||
tenant_id,
|
||||
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
title COLLATE "unicode_ci"
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX idx_documents_tenant_folder_issued_at
|
||||
ON tenant.documents (
|
||||
tenant_id,
|
||||
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
issued_at,
|
||||
title COLLATE "unicode_ci"
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX idx_documents_tenant_folder_created_at
|
||||
ON tenant.documents (
|
||||
tenant_id,
|
||||
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
created_at,
|
||||
title COLLATE "unicode_ci"
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX idx_documents_tenant_folder_updated_at
|
||||
ON tenant.documents (
|
||||
tenant_id,
|
||||
COALESCE(folder_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
updated_at,
|
||||
title COLLATE "unicode_ci"
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE shared.jobs
|
||||
DROP CONSTRAINT jobs_tenant_id_fkey;
|
||||
|
||||
ALTER TABLE shared.jobs
|
||||
ALTER COLUMN tenant_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE shared.jobs
|
||||
ADD CONSTRAINT jobs_tenant_id_fkey
|
||||
FOREIGN KEY (tenant_id)
|
||||
REFERENCES shared.tenants(id);
|
||||
|
||||
ALTER TABLE shared.jobs
|
||||
DROP COLUMN result;
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE shared.jobs
|
||||
ALTER COLUMN tenant_id DROP NOT NULL;
|
||||
|
||||
ALTER TABLE shared.jobs
|
||||
DROP CONSTRAINT jobs_tenant_id_fkey;
|
||||
|
||||
ALTER TABLE shared.jobs
|
||||
ADD CONSTRAINT jobs_tenant_id_fkey
|
||||
FOREIGN KEY (tenant_id)
|
||||
REFERENCES shared.tenants(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE shared.jobs
|
||||
ADD COLUMN result JSONB;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- diesel:run_in_transaction = false
|
||||
|
||||
-- Enum values cannot be removed safely; this down migration intentionally left empty.
|
||||
SELECT 1;
|
||||
@@ -0,0 +1,46 @@
|
||||
-- diesel:run_in_transaction = false
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
EXECUTE 'ALTER TYPE tenant.api_capability ADD VALUE ''tenants:write''';
|
||||
EXCEPTION
|
||||
WHEN undefined_object THEN
|
||||
BEGIN
|
||||
EXECUTE 'ALTER TYPE api_capability ADD VALUE ''tenants:write''';
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END;
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
EXECUTE 'ALTER TYPE tenant.api_capability ADD VALUE ''tenants:reset''';
|
||||
EXCEPTION
|
||||
WHEN undefined_object THEN
|
||||
BEGIN
|
||||
EXECUTE 'ALTER TYPE api_capability ADD VALUE ''tenants:reset''';
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END;
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
EXECUTE 'ALTER TYPE tenant.api_capability ADD VALUE ''tenants:delete''';
|
||||
EXCEPTION
|
||||
WHEN undefined_object THEN
|
||||
BEGIN
|
||||
EXECUTE 'ALTER TYPE api_capability ADD VALUE ''tenants:delete''';
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END;
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END;
|
||||
END $$;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- diesel:run_in_transaction = false
|
||||
|
||||
DELETE FROM tenant.capability_set_capabilities
|
||||
WHERE capability IN (
|
||||
'tenants:write'::api_capability,
|
||||
'tenants:reset'::api_capability,
|
||||
'tenants:delete'::api_capability
|
||||
)
|
||||
AND capability_set_id IN (SELECT id FROM tenant.capability_sets WHERE slug = 'owner');
|
||||
@@ -0,0 +1,22 @@
|
||||
-- diesel:run_in_transaction = false
|
||||
|
||||
WITH owner_sets AS (
|
||||
SELECT id FROM tenant.capability_sets WHERE slug = 'owner'
|
||||
)
|
||||
INSERT INTO tenant.capability_set_capabilities (capability_set_id, capability)
|
||||
SELECT id, 'tenants:write'::api_capability FROM owner_sets
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH owner_sets AS (
|
||||
SELECT id FROM tenant.capability_sets WHERE slug = 'owner'
|
||||
)
|
||||
INSERT INTO tenant.capability_set_capabilities (capability_set_id, capability)
|
||||
SELECT id, 'tenants:reset'::api_capability FROM owner_sets
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH owner_sets AS (
|
||||
SELECT id FROM tenant.capability_sets WHERE slug = 'owner'
|
||||
)
|
||||
INSERT INTO tenant.capability_set_capabilities (capability_set_id, capability)
|
||||
SELECT id, 'tenants:delete'::api_capability FROM owner_sets
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE tenant.document_asset_objects (
|
||||
id UUID PRIMARY KEY,
|
||||
asset_id UUID NOT NULL REFERENCES tenant.document_assets(id) ON DELETE CASCADE,
|
||||
ordinal INT NOT NULL,
|
||||
s3_key TEXT NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
tenant_id UUID NOT NULL REFERENCES shared.tenants(id),
|
||||
CONSTRAINT document_asset_objects_ordinal_positive CHECK (ordinal >= 1),
|
||||
CONSTRAINT document_asset_objects_asset_ordinal_unique UNIQUE (asset_id, ordinal)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_asset_objects_asset_ordinal
|
||||
ON tenant.document_asset_objects(asset_id, ordinal);
|
||||
|
||||
CREATE INDEX document_asset_objects_tenant_id_idx ON tenant.document_asset_objects(tenant_id);
|
||||
|
||||
ALTER TABLE tenant.document_assets ADD COLUMN cardinality INT;
|
||||
UPDATE tenant.document_assets SET cardinality = 1;
|
||||
|
||||
INSERT INTO tenant.document_asset_objects (id, asset_id, ordinal, s3_key, metadata, tenant_id)
|
||||
SELECT gen_random_uuid(), id, 1, s3_key, metadata, tenant_id
|
||||
FROM tenant.document_assets;
|
||||
|
||||
ALTER TABLE tenant.document_assets DROP COLUMN s3_key;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Prevent concurrent inserts/updates during backfill.
|
||||
LOCK TABLE tenant.document_asset_objects IN ACCESS EXCLUSIVE MODE;
|
||||
LOCK TABLE tenant.document_assets IN ACCESS EXCLUSIVE MODE;
|
||||
|
||||
ALTER TABLE tenant.document_assets ADD COLUMN s3_key TEXT;
|
||||
|
||||
UPDATE tenant.document_assets AS da
|
||||
SET s3_key = o.s3_key,
|
||||
metadata = COALESCE(da.metadata, '{}'::jsonb) || COALESCE(o.metadata, '{}'::jsonb)
|
||||
FROM tenant.document_asset_objects AS o
|
||||
WHERE o.asset_id = da.id
|
||||
AND o.ordinal = 1;
|
||||
|
||||
DELETE FROM tenant.document_asset_objects WHERE ordinal <> 1;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM tenant.document_assets WHERE s3_key IS NULL) THEN
|
||||
RAISE EXCEPTION 'cannot drop document_asset_objects: some assets are missing a populated ordinal 1 object (s3_key null)';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
ALTER TABLE tenant.document_assets ALTER COLUMN s3_key SET NOT NULL;
|
||||
ALTER TABLE tenant.document_assets DROP COLUMN cardinality;
|
||||
|
||||
DROP TABLE tenant.document_asset_objects;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Revert column rename.
|
||||
ALTER TABLE tenant.documents
|
||||
RENAME COLUMN mime_type TO content_type;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Rename document content_type column to mime_type for consistency with API.
|
||||
ALTER TABLE tenant.documents
|
||||
RENAME COLUMN content_type TO mime_type;
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE tenant.document_assets
|
||||
SET asset_type = 'ocr-text'
|
||||
WHERE asset_type = 'text-content';
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE tenant.document_assets
|
||||
SET asset_type = 'text-content'
|
||||
WHERE asset_type = 'ocr-text';
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS tenant.idx_documents_title_trgm;
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE INDEX idx_documents_title_trgm
|
||||
ON tenant.documents
|
||||
USING gin (title gin_trgm_ops)
|
||||
WHERE deleted_at IS NULL;
|
||||
Reference in New Issue
Block a user