-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathschema.sql
More file actions
301 lines (253 loc) · 9.06 KB
/
schema.sql
File metadata and controls
301 lines (253 loc) · 9.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
-- required extensions
CREATE EXTENSION IF NOT EXISTS citext;
-- table schemas
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username CITEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
password TEXT NOT NULL,
avatar_path TEXT,
is_admin BOOL NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE rices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(author_id, slug)
);
CREATE TABLE rice_dotfiles (
rice_id UUID PRIMARY KEY REFERENCES rices(id) ON DELETE CASCADE,
file_path TEXT NOT NULL UNIQUE,
download_count INTEGER NOT NULL DEFAULT 0 CHECK (download_count >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE rice_previews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rice_id UUID NOT NULL REFERENCES rices(id) ON DELETE CASCADE,
file_path TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE rice_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rice_id UUID NOT NULL REFERENCES rices(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE rice_stars (
rice_id UUID NOT NULL REFERENCES rices(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
starred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(rice_id, user_id)
);
CREATE TABLE reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reporter_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
reason TEXT NOT NULL,
rice_id UUID REFERENCES rices(id) ON DELETE CASCADE,
comment_id UUID REFERENCES rice_comments(id) ON DELETE CASCADE,
is_closed BOOL NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- make sure that at least one object is referenced
CHECK (
(rice_id IS NOT NULL)::int + (comment_id IS NOT NULL)::int = 1
),
-- create unique key to ensure users dont send duplicated reports
UNIQUE(reporter_id, reason, is_closed)
);
-- logic behind updating the `updated_at` column for all tables
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
-- check if we're updating download_count
IF to_jsonb(NEW) ? 'download_count' THEN
if NEW.download_count > OLD.download_count THEN
RETURN NEW;
END IF;
END IF;
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_tags_updated_at
BEFORE UPDATE ON tags
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_rices_updated_at
BEFORE UPDATE ON rices
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_rice_dotfiles_updated_at
BEFORE UPDATE ON rice_dotfiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_rice_comments_updated_at
BEFORE UPDATE ON rice_comments
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
INSERT INTO tags (name)
VALUES ('AwesomeWM'), ('Arch Linux'), ('KDE'), ('Hyprland'), ('i3'), ('bspwm');
ALTER TABLE rice_dotfiles
ADD COLUMN file_size BIGINT NOT NULL CHECK (file_size > 0);
CREATE TABLE website_variables (
key TEXT PRIMARY KEY CHECK (key ~ '^[a-z0-9_]+$'),
value TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TRIGGER update_website_variables_updated_at
BEFORE UPDATE ON website_variables
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TABLE links (
name TEXT PRIMARY KEY CHECK (name ~ '^[a-z]+$'),
url TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TRIGGER update_links_updated_at
BEFORE UPDATE ON links
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
INSERT INTO website_variables (key, value)
VALUES
('terms_of_service_text', 'Lorem ipsum'),
('privacy_policy_text', 'Lorem ipsum');
INSERT INTO links (name, url)
VALUES
('discord', 'https://discord.com'),
('github', 'https://github.com');
CREATE TABLE user_bans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
admin_id UUID REFERENCES users(id) CHECK (admin_id != user_id),
reason TEXT NOT NULL,
is_revoked BOOL NOT NULL DEFAULT false,
expires_at TIMESTAMPTZ,
banned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
CREATE OR REPLACE FUNCTION update_revoked_at()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.is_revoked IS true THEN
NEW.revoked_at = NOW();
END IF;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
-- update `revoked_at` column in `user_bans` if ban is revoked
CREATE TRIGGER update_user_ban_revoked_at
BEFORE UPDATE OF is_revoked ON user_bans
FOR EACH ROW EXECUTE FUNCTION update_revoked_at();
-- create a view for fetching user data + ban info
CREATE VIEW users_with_ban_status AS
SELECT
u.*,
EXISTS (
SELECT 1
FROM user_bans b
WHERE
b.user_id = u.id
AND (b.expires_at > NOW() OR b.expires_at IS NULL)
AND b.is_revoked = false
) AS is_banned
FROM users u;
-- add state column to rices for manual verification before being publicly visible
CREATE TYPE rice_state AS ENUM (
'waiting',
'accepted'
);
ALTER TABLE rices
ADD COLUMN "state" rice_state NOT NULL DEFAULT 'waiting';
-- create dotfiles type enum and add column to the table
CREATE TYPE dotfiles_type AS ENUM (
'free',
'one-time'
);
ALTER TABLE rice_dotfiles
ADD COLUMN "type" dotfiles_type NOT NULL DEFAULT 'free';
-- add price column to dotfiles
ALTER TABLE rice_dotfiles
ADD COLUMN price NUMERIC(5, 2) NOT NULL DEFAULT 1.0 CHECK (price > 0.0);
-- create table to keep track of dotfiles purchased by users
CREATE TABLE dotfiles_purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
rice_id UUID NOT NULL REFERENCES rices(id),
price_paid NUMERIC(5, 2) NOT NULL CHECK (price_paid > 0.0),
purchased_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- add polar product id to rice dotfiles
ALTER TABLE rice_dotfiles
ADD COLUMN product_id UUID CHECK (product_id IS NOT NULL OR "type" = 'free');
-- create table to keep track of users' subscription
CREATE TABLE user_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) UNIQUE,
current_period_end TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TRIGGER update_user_subscriptions_updated_at
BEFORE UPDATE ON user_subscriptions
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- table to keep record of ALL webhooks that were received by API
CREATE TABLE webhook_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
polar_webhook_id TEXT NOT NULL UNIQUE,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
processed_at TIMESTAMPTZ,
error TEXT,
received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- M-to-M table for rices and tags
CREATE TABLE rice_tag (
rice_id UUID REFERENCES rices(id) ON DELETE CASCADE,
tag_id INT REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (rice_id, tag_id)
);
-- rice download events
CREATE TABLE rice_downloads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rice_id UUID NOT NULL REFERENCES rices(id),
user_id UUID REFERENCES users(id), -- null if triggered by not signed in user
downloaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- leaderboard stuff
CREATE TYPE leaderboard_period AS ENUM (
'week',
'month',
'year'
);
CREATE TABLE rice_leaderboard (
rice_id UUID REFERENCES rices(id) ON DELETE CASCADE,
period leaderboard_period NOT NULL,
position INT NOT NULL CHECK (position > 0),
score BIGINT NOT NULL CHECK (score >= 0),
snapshot_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (rice_id, period)
);
-- user subscription status
CREATE TYPE subscription_status AS ENUM (
'active',
'canceled'
);
ALTER TABLE user_subscriptions
ADD COLUMN status subscription_status NOT NULL;
-- previews => screenshots refactor
ALTER TABLE rice_previews
RENAME TO rice_screenshots;
DROP TRIGGER update_rice_previews_updated_at ON rice_screenshots;