Skip to content

Commit 0ab512b

Browse files
committed
Update consents
1 parent 30c26bb commit 0ab512b

File tree

17 files changed

+2239
-273
lines changed

17 files changed

+2239
-273
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
4+
-- Migration: Improve consent system
5+
-- - Add URL field to consents
6+
-- - Add remote consent tracking fields
7+
-- - Transform user_consents to user_consent_history with rejection tracking
8+
9+
-- 1. Add new columns to consents table
10+
ALTER TABLE consents
11+
ADD COLUMN url VARCHAR(500),
12+
ADD COLUMN managed_by VARCHAR(100),
13+
ADD COLUMN is_remote BOOLEAN DEFAULT false NOT NULL;
14+
15+
CREATE INDEX idx_consents_is_remote ON consents(is_remote) WHERE is_remote = true;
16+
17+
-- 2. Rename user_consents to user_consent_history
18+
ALTER TABLE user_consents RENAME TO user_consent_history;
19+
20+
-- 3. Update ID prefix constraint to accept both UC (legacy) and UH (new) prefixes
21+
ALTER TABLE user_consent_history
22+
DROP CONSTRAINT user_consents_id_check;
23+
24+
ALTER TABLE user_consent_history
25+
ADD CONSTRAINT user_consent_history_id_check
26+
CHECK (id ~ '^U[CH][0-9A-Z]{26}$');
27+
28+
-- 4. Add new columns to user_consent_history
29+
ALTER TABLE user_consent_history
30+
ADD COLUMN action VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
31+
ADD COLUMN consent_key VARCHAR(100) NOT NULL DEFAULT '',
32+
ADD COLUMN occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
33+
ADD COLUMN source VARCHAR(100),
34+
ADD COLUMN external_consent_id VARCHAR(255),
35+
ADD COLUMN external_timestamp TIMESTAMPTZ;
36+
37+
-- 5. Backfill consent_key from consents table
38+
UPDATE user_consent_history uch
39+
SET consent_key = c.key
40+
FROM consents c
41+
WHERE uch.consent_id = c.id;
42+
43+
-- 6. Backfill occurred_at from accepted_at
44+
UPDATE user_consent_history
45+
SET occurred_at = accepted_at;
46+
47+
-- 7. Remove defaults after backfill
48+
ALTER TABLE user_consent_history
49+
ALTER COLUMN consent_key DROP DEFAULT,
50+
ALTER COLUMN action DROP DEFAULT,
51+
ALTER COLUMN occurred_at DROP DEFAULT;
52+
53+
-- 8. Add constraint for action enum
54+
ALTER TABLE user_consent_history
55+
ADD CONSTRAINT check_action CHECK (action IN ('ACCEPTED', 'REJECTED'));
56+
57+
-- 9. Drop old columns
58+
ALTER TABLE user_consent_history
59+
DROP COLUMN accepted_at,
60+
DROP COLUMN created_at;
61+
62+
-- 10. Drop old unique constraint (allow multiple actions over time)
63+
ALTER TABLE user_consent_history
64+
DROP CONSTRAINT user_consents_user_id_consent_id_key;
65+
66+
-- 11. Update indexes
67+
DROP INDEX IF EXISTS idx_user_consents_user;
68+
DROP INDEX IF EXISTS idx_user_consents_consent;
69+
70+
CREATE INDEX idx_user_consent_history_user ON user_consent_history(user_id);
71+
CREATE INDEX idx_user_consent_history_consent ON user_consent_history(consent_id);
72+
CREATE INDEX idx_user_consent_history_key ON user_consent_history(consent_key);
73+
CREATE INDEX idx_user_consent_history_user_key ON user_consent_history(user_id, consent_key);
74+
CREATE INDEX idx_user_consent_history_occurred ON user_consent_history(occurred_at);
75+
76+
-- 12. Add unique index for idempotency of remote consents
77+
CREATE UNIQUE INDEX idx_user_consent_history_remote_idempotent
78+
ON user_consent_history(user_id, consent_key, external_consent_id)
79+
WHERE external_consent_id IS NOT NULL;
80+
81+
-- +goose StatementEnd
82+
83+
-- +goose Down
84+
-- +goose StatementBegin
85+
86+
-- Reverse migration: Revert consent system changes
87+
88+
-- 1. Drop unique remote consent index
89+
DROP INDEX IF EXISTS idx_user_consent_history_remote_idempotent;
90+
91+
-- 2. Drop new indexes
92+
DROP INDEX IF EXISTS idx_user_consent_history_occurred;
93+
DROP INDEX IF EXISTS idx_user_consent_history_user_key;
94+
DROP INDEX IF EXISTS idx_user_consent_history_key;
95+
DROP INDEX IF EXISTS idx_user_consent_history_consent;
96+
DROP INDEX IF EXISTS idx_user_consent_history_user;
97+
98+
-- 3. Recreate old indexes
99+
CREATE INDEX idx_user_consents_user ON user_consent_history(user_id);
100+
CREATE INDEX idx_user_consents_consent ON user_consent_history(consent_id);
101+
102+
-- 4. Add back old columns with data
103+
ALTER TABLE user_consent_history
104+
ADD COLUMN accepted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
105+
ADD COLUMN created_at TIMESTAMPTZ NOT NULL DEFAULT now();
106+
107+
-- 5. Backfill accepted_at from occurred_at
108+
UPDATE user_consent_history
109+
SET accepted_at = occurred_at,
110+
created_at = occurred_at;
111+
112+
-- 6. Drop defaults after backfill
113+
ALTER TABLE user_consent_history
114+
ALTER COLUMN accepted_at DROP DEFAULT,
115+
ALTER COLUMN created_at DROP DEFAULT;
116+
117+
-- 7. Restore unique constraint
118+
ALTER TABLE user_consent_history
119+
ADD CONSTRAINT user_consents_user_id_consent_id_key UNIQUE (user_id, consent_id);
120+
121+
-- 8. Drop action constraint
122+
ALTER TABLE user_consent_history
123+
DROP CONSTRAINT check_action;
124+
125+
-- 9. Drop new columns
126+
ALTER TABLE user_consent_history
127+
DROP COLUMN external_timestamp,
128+
DROP COLUMN external_consent_id,
129+
DROP COLUMN source,
130+
DROP COLUMN occurred_at,
131+
DROP COLUMN consent_key,
132+
DROP COLUMN action;
133+
134+
-- 10. Revert ID prefix constraint (back to UC only)
135+
ALTER TABLE user_consent_history
136+
DROP CONSTRAINT user_consent_history_id_check;
137+
138+
ALTER TABLE user_consent_history
139+
ADD CONSTRAINT user_consents_id_check
140+
CHECK (id ~ '^UC[0-9A-Z]{26}$');
141+
142+
-- 11. Rename table back
143+
ALTER TABLE user_consent_history RENAME TO user_consents;
144+
145+
-- 12. Drop consent table additions
146+
DROP INDEX IF EXISTS idx_consents_is_remote;
147+
148+
ALTER TABLE consents
149+
DROP COLUMN is_remote,
150+
DROP COLUMN managed_by,
151+
DROP COLUMN url;
152+
153+
-- +goose StatementEnd

backend/internal/database/queries/consents.sql

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,116 @@
11
-- Consent queries
22

33
-- name: GetConsentByID :one
4-
SELECT id, key, version, title, body, published_at, created_at, updated_at
4+
SELECT id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at
55
FROM consents WHERE id = @id::text;
66

77
-- name: GetConsentsByIDs :many
8-
SELECT id, key, version, title, body, published_at, created_at, updated_at
8+
SELECT id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at
99
FROM consents WHERE id = ANY(@ids::text[]);
1010

1111
-- name: GetLatestPublishedConsentByKey :one
12-
SELECT id, key, version, title, body, published_at, created_at, updated_at
12+
SELECT id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at
1313
FROM consents
1414
WHERE key = @key::text AND published_at IS NOT NULL AND published_at <= now()
1515
ORDER BY version DESC LIMIT 1;
1616

1717
-- name: GetAllLatestPublishedConsents :many
18-
SELECT DISTINCT ON (key) id, key, version, title, body, published_at, created_at, updated_at
18+
SELECT DISTINCT ON (key) id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at
1919
FROM consents
2020
WHERE published_at IS NOT NULL AND published_at <= now()
2121
ORDER BY key, version DESC;
2222

23+
-- name: GetLatestConsentByKey :one
24+
SELECT id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at
25+
FROM consents
26+
WHERE key = @key::text
27+
ORDER BY version DESC
28+
LIMIT 1;
29+
2330
-- name: CreateConsent :one
24-
INSERT INTO consents (id, key, version, title, body, published_at)
25-
VALUES (@id::text, @key::text, @version::int, @title::text, @body::text, @published_at)
26-
RETURNING id, key, version, title, body, published_at, created_at, updated_at;
31+
INSERT INTO consents (id, key, version, title, body, url, published_at, is_remote, managed_by)
32+
VALUES (@id::text, @key::text, @version::int, @title::text, @body::text, @url, @published_at, @is_remote::bool, @managed_by)
33+
RETURNING id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at;
2734

2835
-- name: UpdateConsent :one
2936
UPDATE consents SET
3037
title = CASE WHEN @title::text = '' THEN title ELSE @title::text END,
3138
body = CASE WHEN @body::text = '' THEN body ELSE @body::text END,
39+
url = CASE WHEN @url::text = '' THEN url ELSE @url::text END,
3240
published_at = @published_at,
3341
updated_at = now()
3442
WHERE id = @id::text
35-
RETURNING id, key, version, title, body, published_at, created_at, updated_at;
43+
RETURNING id, key, version, title, body, url, published_at, is_remote, managed_by, created_at, updated_at;
3644

3745
-- name: GetNextVersionForConsentKey :one
3846
SELECT COALESCE(MAX(version), 0) + 1 as next_version FROM consents WHERE key = @key::text;
3947

40-
-- User consent queries
41-
42-
-- name: GetUserConsentsByUserID :many
43-
SELECT uc.id, uc.user_id, uc.consent_id, uc.accepted_at, uc.created_at,
44-
c.key as consent_key, c.version as consent_version
45-
FROM user_consents uc
46-
INNER JOIN consents c ON uc.consent_id = c.id
47-
WHERE uc.user_id = @user_id::text;
48-
49-
-- name: GetUserConsentsByUserIDs :many
50-
SELECT uc.id, uc.user_id, uc.consent_id, uc.accepted_at, uc.created_at,
51-
c.key as consent_key, c.version as consent_version
52-
FROM user_consents uc
53-
INNER JOIN consents c ON uc.consent_id = c.id
54-
WHERE uc.user_id = ANY(@user_ids::text[]);
55-
56-
-- name: GetUserConsentByUserAndConsent :one
57-
SELECT id, user_id, consent_id, accepted_at, created_at
58-
FROM user_consents WHERE user_id = @user_id::text AND consent_id = @consent_id::text;
59-
60-
-- name: CreateUserConsent :one
61-
INSERT INTO user_consents (id, user_id, consent_id, accepted_at)
62-
VALUES (@id::text, @user_id::text, @consent_id::text, @accepted_at)
63-
RETURNING id, user_id, consent_id, accepted_at, created_at;
64-
65-
-- name: GetMissingConsentsForUser :many
66-
SELECT c.id, c.key, c.version, c.title, c.body, c.published_at, c.created_at, c.updated_at
48+
-- User consent history queries
49+
50+
-- name: CreateUserConsentHistory :one
51+
INSERT INTO user_consent_history (
52+
id, user_id, consent_id, consent_key, action, occurred_at,
53+
source, external_consent_id, external_timestamp
54+
)
55+
VALUES (
56+
@id::text, @user_id::text, @consent_id::text, @consent_key::text,
57+
@action::text, @occurred_at, @source, @external_consent_id, @external_timestamp
58+
)
59+
RETURNING id, user_id, consent_id, consent_key, action, occurred_at,
60+
source, external_consent_id, external_timestamp;
61+
62+
-- name: GetLatestUserConsentActionByKey :one
63+
SELECT id, user_id, consent_id, consent_key, action, occurred_at,
64+
source, external_consent_id, external_timestamp
65+
FROM user_consent_history
66+
WHERE user_id = @user_id::text AND consent_key = @consent_key::text
67+
ORDER BY occurred_at DESC
68+
LIMIT 1;
69+
70+
-- name: GetCurrentUserConsentStatusesByUsers :many
71+
-- Gets the latest action for each consent key for multiple users
72+
SELECT DISTINCT ON (user_id, consent_key)
73+
id, user_id, consent_id, consent_key, action, occurred_at, source
74+
FROM user_consent_history
75+
WHERE user_id = ANY(@user_ids::text[])
76+
ORDER BY user_id, consent_key, occurred_at DESC;
77+
78+
-- name: GetUserConsentHistoryByUserAndKey :many
79+
SELECT id, user_id, consent_id, consent_key, action, occurred_at,
80+
source, external_consent_id, external_timestamp
81+
FROM user_consent_history
82+
WHERE user_id = @user_id::text AND consent_key = @consent_key::text
83+
ORDER BY occurred_at DESC;
84+
85+
-- name: GetUserConsentHistoryByUser :many
86+
SELECT id, user_id, consent_id, consent_key, action, occurred_at,
87+
source, external_consent_id, external_timestamp
88+
FROM user_consent_history
89+
WHERE user_id = @user_id::text
90+
ORDER BY occurred_at DESC;
91+
92+
-- name: GetUserConsentHistoryByUsers :many
93+
SELECT id, user_id, consent_id, consent_key, action, occurred_at,
94+
source, external_consent_id, external_timestamp
95+
FROM user_consent_history
96+
WHERE user_id = ANY(@user_ids::text[])
97+
ORDER BY user_id, occurred_at DESC;
98+
99+
-- name: GetMissingConsentsForUserWithRejections :many
100+
-- Gets consents that user has never acted upon (no history)
101+
SELECT c.id, c.key, c.version, c.title, c.body, c.url, c.published_at,
102+
c.is_remote, c.managed_by, c.created_at, c.updated_at
67103
FROM (
68-
SELECT DISTINCT ON (key) id, key, version, title, body, published_at, created_at, updated_at
104+
SELECT DISTINCT ON (key) id, key, version, title, body, url, published_at,
105+
is_remote, managed_by, created_at, updated_at
69106
FROM consents
70107
WHERE published_at IS NOT NULL AND published_at <= now()
71108
ORDER BY key, version DESC
72109
) c
73110
WHERE NOT EXISTS (
74-
SELECT 1 FROM user_consents uc WHERE uc.user_id = @user_id::text AND uc.consent_id = c.id
111+
SELECT 1 FROM user_consent_history uch
112+
WHERE uch.user_id = @user_id::text
113+
AND uch.consent_key = c.key
75114
);
76115

77116
-- Translation queries

0 commit comments

Comments
 (0)