Bump secrets-mcp to 0.3.8 (tag 0.3.7 already used). - Junction table entry_secrets; secrets user-scoped with type - Per-user unique secrets.name; link_secret_names on add - Manual migrations + migrate script; MCP/tool and Web updates Made-with: Cursor
127 lines
4.0 KiB
PL/PgSQL
127 lines
4.0 KiB
PL/PgSQL
-- Entry-Secret N:N migration (manual SQL)
|
|
-- Safe to re-run: uses IF EXISTS/IF NOT EXISTS guards.
|
|
|
|
BEGIN;
|
|
|
|
-- 1) secrets: add new columns
|
|
ALTER TABLE secrets
|
|
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE secrets
|
|
ADD COLUMN IF NOT EXISTS type VARCHAR(64) NOT NULL DEFAULT 'text';
|
|
|
|
-- 2) rename field_name -> name (idempotent)
|
|
DO $$ BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'secrets' AND column_name = 'field_name'
|
|
) THEN
|
|
ALTER TABLE secrets RENAME COLUMN field_name TO name;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- 3) create join table
|
|
CREATE TABLE IF NOT EXISTS entry_secrets (
|
|
entry_id UUID NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
|
secret_id UUID NOT NULL REFERENCES secrets(id) ON DELETE CASCADE,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (entry_id, secret_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_entry_secrets_secret_id ON entry_secrets(secret_id);
|
|
|
|
-- 4) backfill user_id and relationship from old secrets.entry_id
|
|
DO $$ BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'secrets' AND column_name = 'entry_id'
|
|
) THEN
|
|
UPDATE secrets s
|
|
SET user_id = e.user_id
|
|
FROM entries e
|
|
WHERE s.entry_id = e.id AND s.user_id IS NULL;
|
|
|
|
INSERT INTO entry_secrets(entry_id, secret_id, sort_order)
|
|
SELECT entry_id, id, 0
|
|
FROM secrets
|
|
WHERE entry_id IS NOT NULL
|
|
ON CONFLICT DO NOTHING;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- 5) backfill secret types
|
|
UPDATE secrets SET type = 'pem' WHERE name IN ('ssh_key');
|
|
UPDATE secrets SET type = 'password' WHERE name IN ('password');
|
|
UPDATE secrets SET type = 'phone' WHERE name LIKE 'phone%';
|
|
UPDATE secrets SET type = 'url' WHERE name IN ('webhook_url', 'address');
|
|
UPDATE secrets
|
|
SET type = 'token'
|
|
WHERE name IN (
|
|
'access_key_id',
|
|
'access_key_secret',
|
|
'global_api_key',
|
|
'api_key',
|
|
'secret_key',
|
|
'personal_access_token',
|
|
'runner_token',
|
|
'GOOGLE_CLIENT_ID',
|
|
'GOOGLE_CLIENT_SECRET'
|
|
);
|
|
|
|
-- 6) drop old entry_id path
|
|
ALTER TABLE secrets DROP CONSTRAINT IF EXISTS secrets_entry_id_fkey;
|
|
DROP INDEX IF EXISTS idx_secrets_entry_id;
|
|
ALTER TABLE secrets DROP CONSTRAINT IF EXISTS secrets_entry_id_field_name_key;
|
|
ALTER TABLE secrets DROP CONSTRAINT IF EXISTS secrets_entry_id_name_key;
|
|
ALTER TABLE secrets DROP COLUMN IF EXISTS entry_id;
|
|
|
|
-- 7) add indexes for new access paths
|
|
CREATE INDEX IF NOT EXISTS idx_secrets_user_id
|
|
ON secrets(user_id) WHERE user_id IS NOT NULL;
|
|
DO $$
|
|
DECLARE
|
|
duplicate_samples TEXT;
|
|
BEGIN
|
|
SELECT string_agg(
|
|
format('user_id=%s, name=%s, count=%s', t.user_id, t.name, t.cnt),
|
|
E'\n'
|
|
)
|
|
INTO duplicate_samples
|
|
FROM (
|
|
SELECT user_id::TEXT AS user_id, name, COUNT(*) AS cnt
|
|
FROM secrets
|
|
WHERE user_id IS NOT NULL
|
|
GROUP BY user_id, name
|
|
HAVING COUNT(*) > 1
|
|
ORDER BY cnt DESC, user_id, name
|
|
LIMIT 20
|
|
) t;
|
|
|
|
IF duplicate_samples IS NOT NULL THEN
|
|
RAISE EXCEPTION
|
|
'Cannot enforce unique constraint on secrets(user_id, name). Duplicates found:%',
|
|
E'\n' || duplicate_samples
|
|
USING HINT = 'Please deduplicate conflicting rows, then rerun migration.';
|
|
END IF;
|
|
END $$;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_secrets_unique_user_name
|
|
ON secrets(user_id, name) WHERE user_id IS NOT NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_secrets_name ON secrets(name);
|
|
CREATE INDEX IF NOT EXISTS idx_secrets_type ON secrets(type);
|
|
|
|
-- 8) secrets_history: rename and remove entry-scoped columns
|
|
DO $$ BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'secrets_history' AND column_name = 'field_name'
|
|
) THEN
|
|
ALTER TABLE secrets_history RENAME COLUMN field_name TO name;
|
|
END IF;
|
|
END $$;
|
|
ALTER TABLE secrets_history DROP COLUMN IF EXISTS entry_id;
|
|
ALTER TABLE secrets_history DROP COLUMN IF EXISTS entry_version;
|
|
|
|
COMMIT;
|