@pgpm/achievements 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +22 -0
  2. package/Makefile +6 -0
  3. package/README.md +5 -0
  4. package/__tests__/__snapshots__/achievements.test.ts.snap +180 -0
  5. package/__tests__/__snapshots__/triggers.test.ts.snap +112 -0
  6. package/__tests__/achievements.test.ts +250 -0
  7. package/__tests__/triggers.test.ts +167 -0
  8. package/deploy/schemas/status_private/procedures/status_triggers.sql +148 -0
  9. package/deploy/schemas/status_private/procedures/upsert_achievement.sql +24 -0
  10. package/deploy/schemas/status_private/procedures/user_completed_step.sql +16 -0
  11. package/deploy/schemas/status_private/procedures/user_incompleted_step.sql +22 -0
  12. package/deploy/schemas/status_private/schema.sql +15 -0
  13. package/deploy/schemas/status_public/procedures/steps_required.sql +59 -0
  14. package/deploy/schemas/status_public/procedures/user_achieved.sql +29 -0
  15. package/deploy/schemas/status_public/schema.sql +15 -0
  16. package/deploy/schemas/status_public/tables/level_requirements/table.sql +21 -0
  17. package/deploy/schemas/status_public/tables/levels/table.sql +15 -0
  18. package/deploy/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql +11 -0
  19. package/deploy/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql +38 -0
  20. package/deploy/schemas/status_public/tables/user_achievements/table.sql +20 -0
  21. package/deploy/schemas/status_public/tables/user_levels/table.sql +20 -0
  22. package/deploy/schemas/status_public/tables/user_steps/table.sql +18 -0
  23. package/deploy/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql +25 -0
  24. package/jest.config.js +15 -0
  25. package/launchql-achievements.control +8 -0
  26. package/launchql.plan +20 -0
  27. package/package.json +29 -0
  28. package/revert/schemas/status_private/procedures/status_triggers.sql +10 -0
  29. package/revert/schemas/status_private/procedures/upsert_achievement.sql +7 -0
  30. package/revert/schemas/status_private/procedures/user_completed_step.sql +7 -0
  31. package/revert/schemas/status_private/procedures/user_incompleted_step.sql +7 -0
  32. package/revert/schemas/status_private/schema.sql +7 -0
  33. package/revert/schemas/status_public/procedures/steps_required.sql +7 -0
  34. package/revert/schemas/status_public/procedures/user_achieved.sql +7 -0
  35. package/revert/schemas/status_public/schema.sql +7 -0
  36. package/revert/schemas/status_public/tables/level_requirements/table.sql +7 -0
  37. package/revert/schemas/status_public/tables/levels/table.sql +7 -0
  38. package/revert/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql +8 -0
  39. package/revert/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql +18 -0
  40. package/revert/schemas/status_public/tables/user_achievements/table.sql +7 -0
  41. package/revert/schemas/status_public/tables/user_levels/table.sql +7 -0
  42. package/revert/schemas/status_public/tables/user_steps/table.sql +7 -0
  43. package/revert/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql +8 -0
  44. package/sqitch.plan +20 -0
  45. package/sql/launchql-achievements--0.4.6.sql +264 -0
  46. package/verify/schemas/status_private/procedures/status_triggers.sql +10 -0
  47. package/verify/schemas/status_private/procedures/upsert_achievement.sql +7 -0
  48. package/verify/schemas/status_private/procedures/user_completed_step.sql +7 -0
  49. package/verify/schemas/status_private/procedures/user_incompleted_step.sql +7 -0
  50. package/verify/schemas/status_private/schema.sql +7 -0
  51. package/verify/schemas/status_public/procedures/steps_required.sql +7 -0
  52. package/verify/schemas/status_public/procedures/user_achieved.sql +7 -0
  53. package/verify/schemas/status_public/schema.sql +7 -0
  54. package/verify/schemas/status_public/tables/level_requirements/table.sql +7 -0
  55. package/verify/schemas/status_public/tables/levels/table.sql +7 -0
  56. package/verify/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql +7 -0
  57. package/verify/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql +15 -0
  58. package/verify/schemas/status_public/tables/user_achievements/table.sql +7 -0
  59. package/verify/schemas/status_public/tables/user_levels/table.sql +7 -0
  60. package/verify/schemas/status_public/tables/user_steps/table.sql +7 -0
  61. package/verify/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql +8 -0
@@ -0,0 +1,167 @@
1
+ import { getConnections, PgTestClient } from 'pgsql-test';
2
+ import { snapshot } from 'graphile-test';
3
+
4
+ let pg: PgTestClient;
5
+ let teardown: () => Promise<void>;
6
+
7
+ const user_id = 'b9d22af1-62c7-43a5-b8c4-50630bbd4962';
8
+
9
+ const levels = ['newbie', 'advanced'];
10
+
11
+ const newbie = [
12
+ ['upload_profile_image'],
13
+ ['complete_action', 5],
14
+ ['accept_cookies'],
15
+ ['accept_privacy'],
16
+ ['agree_to_terms']
17
+ ];
18
+ const advanced = [
19
+ ['invite_users', 15],
20
+ ['complete_action', 15]
21
+ ];
22
+
23
+ beforeAll(async () => {
24
+ ({ pg, teardown } = await getConnections());
25
+
26
+ await pg.any(`CREATE TABLE status_public.mytable (
27
+ id serial,
28
+ name text,
29
+ toggle text,
30
+ is_approved boolean,
31
+ is_verified boolean default false
32
+ );`);
33
+
34
+ await pg.any(`CREATE TRIGGER mytable_tg1
35
+ BEFORE INSERT ON status_public.mytable
36
+ FOR EACH ROW
37
+ EXECUTE FUNCTION status_private.tg_achievement('name', 'tg_achievement');`);
38
+
39
+ await pg.any(`CREATE TRIGGER mytable_tg2
40
+ BEFORE UPDATE ON status_public.mytable
41
+ FOR EACH ROW
42
+ WHEN (NEW.name IS DISTINCT FROM OLD.name)
43
+ EXECUTE FUNCTION status_private.tg_achievement('name', 'tg_achievement');`);
44
+
45
+ await pg.any(`CREATE TRIGGER mytable_tg3
46
+ BEFORE INSERT ON status_public.mytable
47
+ FOR EACH ROW
48
+ EXECUTE FUNCTION status_private.tg_achievement_toggle('toggle', 'tg_achievement_toggle');`);
49
+
50
+ await pg.any(`CREATE TRIGGER mytable_tg4
51
+ BEFORE UPDATE ON status_public.mytable
52
+ FOR EACH ROW
53
+ WHEN (NEW.toggle IS DISTINCT FROM OLD.toggle)
54
+ EXECUTE FUNCTION status_private.tg_achievement_toggle('toggle', 'tg_achievement_toggle');`);
55
+
56
+ await pg.any(`CREATE TRIGGER mytable_tg5
57
+ BEFORE INSERT ON status_public.mytable
58
+ FOR EACH ROW
59
+ EXECUTE FUNCTION status_private.tg_achievement_boolean('is_approved', 'tg_achievement_boolean');`);
60
+
61
+ await pg.any(`CREATE TRIGGER mytable_tg6
62
+ BEFORE UPDATE ON status_public.mytable
63
+ FOR EACH ROW
64
+ WHEN (NEW.is_approved IS DISTINCT FROM OLD.is_approved)
65
+ EXECUTE FUNCTION status_private.tg_achievement_boolean('is_approved', 'tg_achievement_boolean');`);
66
+
67
+ await pg.any(`CREATE TRIGGER mytable_tg7
68
+ BEFORE INSERT ON status_public.mytable
69
+ FOR EACH ROW
70
+ EXECUTE FUNCTION status_private.tg_achievement_toggle_boolean('is_verified', 'tg_achievement_toggle_boolean');`);
71
+
72
+ await pg.any(`CREATE TRIGGER mytable_tg8
73
+ BEFORE UPDATE ON status_public.mytable
74
+ FOR EACH ROW
75
+ WHEN (NEW.is_verified IS DISTINCT FROM OLD.is_verified)
76
+ EXECUTE FUNCTION status_private.tg_achievement_toggle_boolean('is_verified', 'tg_achievement_toggle_boolean');`);
77
+
78
+ await pg.setContext({
79
+ 'jwt.claims.user_id': user_id
80
+ });
81
+ });
82
+
83
+ afterAll(async () => {
84
+ await teardown();
85
+ });
86
+
87
+ beforeEach(async () => {
88
+ await pg.beforeEach();
89
+
90
+ for (const name of levels) {
91
+ await pg.any(
92
+ `INSERT INTO status_public.levels (name) VALUES ($1) ON CONFLICT DO NOTHING`,
93
+ [name]
94
+ );
95
+ }
96
+
97
+ for (const [name, required_count = 1] of newbie) {
98
+ await pg.any(
99
+ `INSERT INTO status_public.level_requirements (name, level, required_count)
100
+ VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
101
+ [name, 'newbie', required_count]
102
+ );
103
+ }
104
+ for (const [name, required_count = 1] of advanced) {
105
+ await pg.any(
106
+ `INSERT INTO status_public.level_requirements (name, level, required_count)
107
+ VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
108
+ [name, 'advanced', required_count]
109
+ );
110
+ }
111
+ });
112
+
113
+ afterEach(async () => {
114
+ await pg.afterEach();
115
+ });
116
+
117
+ it('newbie', async () => {
118
+ const beforeInsert = await pg.any(
119
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
120
+ );
121
+ expect(snapshot({ beforeInsert })).toMatchSnapshot();
122
+
123
+ await pg.any(
124
+ `INSERT INTO status_public.mytable (name) VALUES ($1)`,
125
+ ['upload_profile_image']
126
+ );
127
+
128
+ const afterFirstInsert = await pg.any(
129
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
130
+ );
131
+ expect(snapshot({ afterFirstInsert })).toMatchSnapshot();
132
+
133
+ await pg.any(`UPDATE status_public.mytable SET toggle = 'yo'`);
134
+
135
+ const afterUpdateToggleToValue = await pg.any(
136
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
137
+ );
138
+ expect(snapshot({ afterUpdateToggleToValue })).toMatchSnapshot();
139
+
140
+ await pg.any(`UPDATE status_public.mytable SET toggle = NULL`);
141
+
142
+ const afterUpdateToggleToNull = await pg.any(
143
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
144
+ );
145
+ expect(snapshot({ afterUpdateToggleToNull })).toMatchSnapshot();
146
+
147
+ await pg.any(`UPDATE status_public.mytable SET is_verified = TRUE`);
148
+
149
+ const afterIsVerifiedIsTrue = await pg.any(
150
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
151
+ );
152
+ expect(snapshot({ afterIsVerifiedIsTrue })).toMatchSnapshot();
153
+
154
+ await pg.any(`UPDATE status_public.mytable SET is_verified = FALSE`);
155
+
156
+ const afterIsVerifiedIsFalse = await pg.any(
157
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
158
+ );
159
+ expect(snapshot({ afterIsVerifiedIsFalse })).toMatchSnapshot();
160
+
161
+ await pg.any(`UPDATE status_public.mytable SET is_approved = TRUE`);
162
+
163
+ const afterIsApprovedTrue = await pg.any(
164
+ `SELECT * FROM status_public.user_achievements ORDER BY name`
165
+ );
166
+ expect(snapshot({ afterIsApprovedTrue })).toMatchSnapshot();
167
+ });
@@ -0,0 +1,148 @@
1
+ -- Deploy schemas/status_private/procedures/status_triggers to pg
2
+
3
+ -- requires: schemas/status_private/schema
4
+ -- requires: schemas/status_private/procedures/user_completed_step
5
+ -- requires: schemas/status_private/procedures/user_incompleted_step
6
+
7
+ BEGIN;
8
+
9
+ CREATE FUNCTION status_private.tg_achievement ()
10
+ RETURNS TRIGGER
11
+ AS $$
12
+ DECLARE
13
+ is_null boolean;
14
+ task_name text;
15
+ BEGIN
16
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
17
+ task_name = TG_ARGV[1]::text;
18
+ EXECUTE format('SELECT ($1).%s IS NULL', TG_ARGV[0])
19
+ USING NEW INTO is_null;
20
+ IF (is_null IS FALSE) THEN
21
+ PERFORM status_private.user_completed_step(task_name);
22
+ END IF;
23
+ RETURN NEW;
24
+ END IF;
25
+ END;
26
+ $$
27
+ LANGUAGE 'plpgsql'
28
+ VOLATILE;
29
+
30
+ CREATE FUNCTION status_private.tg_achievement_toggle ()
31
+ RETURNS TRIGGER
32
+ AS $$
33
+ DECLARE
34
+ is_null boolean;
35
+ task_name text;
36
+ BEGIN
37
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
38
+ task_name = TG_ARGV[1]::text;
39
+ EXECUTE format('SELECT ($1).%s IS NULL', TG_ARGV[0])
40
+ USING NEW INTO is_null;
41
+ IF (is_null IS TRUE) THEN
42
+ PERFORM status_private.user_incompleted_step(task_name);
43
+ ELSE
44
+ PERFORM status_private.user_completed_step(task_name);
45
+ END IF;
46
+ RETURN NEW;
47
+ END IF;
48
+ END;
49
+ $$
50
+ LANGUAGE 'plpgsql'
51
+ VOLATILE;
52
+
53
+ CREATE FUNCTION status_private.tg_achievement_boolean ()
54
+ RETURNS TRIGGER
55
+ AS $$
56
+ DECLARE
57
+ is_true boolean;
58
+ task_name text;
59
+ BEGIN
60
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
61
+ task_name = TG_ARGV[1]::text;
62
+ EXECUTE format('SELECT ($1).%s IS TRUE', TG_ARGV[0])
63
+ USING NEW INTO is_true;
64
+ IF (is_true IS TRUE) THEN
65
+ PERFORM status_private.user_completed_step(task_name);
66
+ END IF;
67
+ RETURN NEW;
68
+ END IF;
69
+ END;
70
+ $$
71
+ LANGUAGE 'plpgsql'
72
+ VOLATILE;
73
+
74
+ CREATE FUNCTION status_private.tg_achievement_toggle_boolean ()
75
+ RETURNS TRIGGER
76
+ AS $$
77
+ DECLARE
78
+ is_true boolean;
79
+ task_name text;
80
+ BEGIN
81
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
82
+ task_name = TG_ARGV[1]::text;
83
+ EXECUTE format('SELECT ($1).%s IS TRUE', TG_ARGV[0])
84
+ USING NEW INTO is_true;
85
+ IF (is_true IS TRUE) THEN
86
+ PERFORM status_private.user_completed_step(task_name);
87
+ ELSE
88
+ PERFORM status_private.user_incompleted_step(task_name);
89
+ END IF;
90
+ RETURN NEW;
91
+ END IF;
92
+ END;
93
+ $$
94
+ LANGUAGE 'plpgsql'
95
+ VOLATILE;
96
+
97
+ -- CREATE FUNCTION app_private.tg_achievement_using_field ()
98
+ -- RETURNS TRIGGER
99
+ -- AS $$
100
+ -- DECLARE
101
+ -- is_null boolean;
102
+ -- task_name citext;
103
+ -- user_id uuid;
104
+ -- BEGIN
105
+ -- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
106
+ -- task_name = TG_ARGV[1]::citext;
107
+ -- EXECUTE format('SELECT ($1).%s IS NULL', TG_ARGV[0])
108
+ -- USING NEW INTO is_null;
109
+ -- EXECUTE format('SELECT ($1).%s::uuid', TG_ARGV[2])
110
+ -- USING NEW INTO user_id;
111
+ -- IF (is_null IS FALSE) THEN
112
+ -- PERFORM app_private.user_completed_task(task_name, user_id);
113
+ -- END IF;
114
+ -- RETURN NEW;
115
+ -- END IF;
116
+ -- END;
117
+ -- $$
118
+ -- LANGUAGE 'plpgsql'
119
+ -- VOLATILE;
120
+
121
+ -- CREATE FUNCTION app_private.tg_achievement_toggle_using_field ()
122
+ -- RETURNS TRIGGER
123
+ -- AS $$
124
+ -- DECLARE
125
+ -- is_null boolean;
126
+ -- task_name citext;
127
+ -- user_id uuid;
128
+ -- BEGIN
129
+ -- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
130
+ -- task_name = TG_ARGV[1]::citext;
131
+ -- EXECUTE format('SELECT ($1).%s IS NULL', TG_ARGV[0])
132
+ -- USING NEW INTO is_null;
133
+ -- EXECUTE format('SELECT ($1).%s::uuid', TG_ARGV[2])
134
+ -- USING NEW INTO user_id;
135
+ -- IF (is_null IS TRUE) THEN
136
+ -- PERFORM app_private.user_incompleted_task(task_name, user_id);
137
+ -- ELSE
138
+ -- PERFORM app_private.user_completed_task(task_name, user_id);
139
+ -- END IF;
140
+ -- RETURN NEW;
141
+ -- END IF;
142
+ -- END;
143
+ -- $$
144
+ -- LANGUAGE 'plpgsql'
145
+ -- VOLATILE;
146
+
147
+
148
+ COMMIT;
@@ -0,0 +1,24 @@
1
+ -- Deploy schemas/status_private/procedures/upsert_achievement to pg
2
+
3
+ -- requires: schemas/status_private/schema
4
+ -- requires: schemas/status_public/tables/user_achievements/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE FUNCTION status_private.upsert_achievement(
9
+ vuser_id uuid, vname text, vcount int
10
+ ) returns void as $$
11
+ BEGIN
12
+ INSERT INTO status_public.user_achievements (user_id, name, count)
13
+ VALUES
14
+ (vuser_id, vname, GREATEST(vcount, 0))
15
+ ON CONFLICT ON CONSTRAINT user_achievements_unique_key
16
+ DO UPDATE SET
17
+ -- look ma! you can actually do aliases inside on conflict
18
+ count = user_achievements.count + EXCLUDED.count
19
+ ;
20
+ END;
21
+ $$
22
+ LANGUAGE 'plpgsql' VOLATILE;
23
+
24
+ COMMIT;
@@ -0,0 +1,16 @@
1
+ -- Deploy schemas/status_private/procedures/user_completed_step to pg
2
+
3
+ -- requires: schemas/status_private/schema
4
+ -- requires: schemas/status_public/tables/user_steps/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE FUNCTION status_private.user_completed_step (
9
+ step text,
10
+ user_id uuid DEFAULT jwt_public.current_user_id()
11
+ ) RETURNS void AS $EOFCODE$
12
+ INSERT INTO status_public.user_steps ( name, user_id, count )
13
+ VALUES ( step, user_id, 1 );
14
+ $EOFCODE$ LANGUAGE sql VOLATILE SECURITY DEFINER;
15
+
16
+ COMMIT;
@@ -0,0 +1,22 @@
1
+ -- Deploy schemas/status_private/procedures/user_incompleted_step to pg
2
+
3
+ -- requires: schemas/status_private/schema
4
+ -- requires: schemas/status_public/tables/user_steps/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE FUNCTION status_private.user_incompleted_step (
9
+ step text,
10
+ user_id uuid DEFAULT jwt_public.current_user_id()
11
+ ) RETURNS void AS $EOFCODE$
12
+ BEGIN
13
+ DELETE FROM status_public.user_steps s
14
+ WHERE s.user_id = user_incompleted_step.user_id
15
+ AND s.name = step;
16
+ DELETE FROM status_public.user_achievements a
17
+ WHERE a.user_id = user_incompleted_step.user_id
18
+ AND a.name = step;
19
+ END;
20
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
21
+
22
+ COMMIT;
@@ -0,0 +1,15 @@
1
+ -- Deploy schemas/status_private/schema to pg
2
+
3
+
4
+ BEGIN;
5
+
6
+ CREATE SCHEMA IF NOT EXISTS status_private;
7
+
8
+ GRANT USAGE ON SCHEMA status_private
9
+ TO authenticated, anonymous;
10
+
11
+ ALTER DEFAULT PRIVILEGES IN SCHEMA status_private
12
+ GRANT EXECUTE ON FUNCTIONS
13
+ TO authenticated;
14
+
15
+ COMMIT;
@@ -0,0 +1,59 @@
1
+ -- Deploy schemas/status_public/procedures/steps_required to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+ -- requires: schemas/status_public/tables/level_requirements/table
5
+ -- requires: schemas/status_public/tables/user_achievements/table
6
+
7
+ BEGIN;
8
+
9
+ -- good for debugging...
10
+
11
+ -- SELECT
12
+ -- level_requirements.name,
13
+ -- level_requirements.level,
14
+
15
+ -- coalesce(user_achievements.count,0) as completed,
16
+ -- level_requirements.required_count as required,
17
+ -- -1*(coalesce(user_achievements.count,0)-level_requirements.required_count) as count
18
+
19
+ -- FROM
20
+ -- status_public.level_requirements
21
+ -- FULL OUTER JOIN status_public.user_achievements ON (
22
+ -- user_achievements.name = level_requirements.name
23
+ -- AND user_achievements.user_id ='b9d22af1-62c7-43a5-b8c4-50630bbd4962'
24
+ -- )
25
+ -- JOIN status_public.levels ON (level_requirements.level = levels.name)
26
+ -- ;
27
+
28
+ CREATE FUNCTION status_public.steps_required(
29
+ vlevel text,
30
+ vrole_id uuid DEFAULT jwt_public.current_user_id()
31
+ )
32
+ RETURNS SETOF status_public.level_requirements
33
+ AS $$
34
+ BEGIN
35
+ RETURN QUERY
36
+ SELECT
37
+ level_requirements.id,
38
+ level_requirements.name,
39
+ level_requirements.level,
40
+ -1*(coalesce(user_achievements.count,0)-level_requirements.required_count) as required_count,
41
+ level_requirements.priority
42
+ FROM
43
+ status_public.level_requirements
44
+ FULL OUTER JOIN status_public.user_achievements ON (
45
+ user_achievements.name = level_requirements.name
46
+ AND user_achievements.user_id =vrole_id
47
+ )
48
+ JOIN status_public.levels ON (level_requirements.level = levels.name)
49
+ WHERE
50
+ level_requirements.level = vlevel
51
+ AND -1*(coalesce(user_achievements.count,0)-level_requirements.required_count) > 0
52
+ ORDER BY priority ASC
53
+ ;
54
+ END;
55
+ $$
56
+ LANGUAGE 'plpgsql'
57
+ STABLE;
58
+ COMMIT;
59
+
@@ -0,0 +1,29 @@
1
+ -- Deploy schemas/status_public/procedures/user_achieved to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+ -- requires: schemas/status_public/procedures/steps_required
5
+ -- requires: schemas/status_public/tables/level_requirements/table
6
+ -- requires: schemas/status_public/tables/user_achievements/table
7
+
8
+ BEGIN;
9
+
10
+ CREATE FUNCTION status_public.user_achieved(
11
+ vlevel text,
12
+ vrole_id uuid DEFAULT jwt_public.current_user_id()
13
+ ) returns boolean as $$
14
+ DECLARE
15
+ c int;
16
+ BEGIN
17
+ SELECT COUNT(*) FROM
18
+ status_public.steps_required(
19
+ vlevel,
20
+ vrole_id
21
+ )
22
+ INTO c;
23
+
24
+ RETURN c <= 0;
25
+ END;
26
+ $$
27
+ LANGUAGE 'plpgsql' STABLE;
28
+
29
+ COMMIT;
@@ -0,0 +1,15 @@
1
+ -- Deploy schemas/status_public/schema to pg
2
+
3
+
4
+ BEGIN;
5
+
6
+ CREATE SCHEMA IF NOT EXISTS status_public;
7
+
8
+ GRANT USAGE ON SCHEMA status_public
9
+ TO authenticated, anonymous;
10
+
11
+ ALTER DEFAULT PRIVILEGES IN SCHEMA status_public
12
+ GRANT EXECUTE ON FUNCTIONS
13
+ TO authenticated;
14
+
15
+ COMMIT;
@@ -0,0 +1,21 @@
1
+ -- Deploy schemas/status_public/tables/level_requirements/table to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+ -- requires: schemas/status_public/tables/levels/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE TABLE status_public.level_requirements (
9
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (),
10
+ name text NOT NULL,
11
+ level text NOT NULL,
12
+ required_count int DEFAULT 1,
13
+ priority int DEFAULT 100,
14
+ unique(name, level)
15
+ );
16
+
17
+ COMMENT ON TABLE status_public.level_requirements IS 'Requirements to achieve a level';
18
+ CREATE INDEX ON status_public.level_requirements (name, level, priority);
19
+ GRANT SELECT ON TABLE status_public.levels TO authenticated;
20
+
21
+ COMMIT;
@@ -0,0 +1,15 @@
1
+ -- Deploy schemas/status_public/tables/levels/table to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+
5
+ BEGIN;
6
+
7
+ CREATE TABLE status_public.levels (
8
+ name text NOT NULL PRIMARY KEY
9
+ );
10
+
11
+ COMMENT ON TABLE status_public.levels IS 'Levels for achievement';
12
+
13
+ GRANT SELECT ON TABLE status_public.levels TO public;
14
+
15
+ COMMIT;
@@ -0,0 +1,11 @@
1
+ -- Deploy schemas/status_public/tables/user_achievements/policies/enable_row_level_security to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+ -- requires: schemas/status_public/tables/user_achievements/table
5
+
6
+ BEGIN;
7
+
8
+ ALTER TABLE status_public.user_achievements
9
+ ENABLE ROW LEVEL SECURITY;
10
+
11
+ COMMIT;
@@ -0,0 +1,38 @@
1
+ -- Deploy schemas/status_public/tables/user_achievements/policies/user_achievements_policy to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+ -- requires: schemas/status_public/tables/user_achievements/table
5
+ -- requires: schemas/status_public/tables/user_achievements/policies/enable_row_level_security
6
+
7
+ BEGIN;
8
+
9
+ CREATE POLICY can_select_user_achievements ON status_public.user_achievements
10
+ FOR SELECT
11
+ USING (
12
+ jwt_public.current_user_id() = user_id
13
+ );
14
+
15
+ CREATE POLICY can_insert_user_achievements ON status_public.user_achievements
16
+ FOR INSERT
17
+ WITH CHECK (
18
+ FALSE
19
+ );
20
+
21
+ CREATE POLICY can_update_user_achievements ON status_public.user_achievements
22
+ FOR UPDATE
23
+ USING (
24
+ FALSE
25
+ );
26
+
27
+ CREATE POLICY can_delete_user_achievements ON status_public.user_achievements
28
+ FOR DELETE
29
+ USING (
30
+ FALSE
31
+ );
32
+
33
+ GRANT INSERT ON TABLE status_public.user_achievements TO authenticated;
34
+ GRANT SELECT ON TABLE status_public.user_achievements TO authenticated;
35
+ GRANT UPDATE ON TABLE status_public.user_achievements TO authenticated;
36
+ GRANT DELETE ON TABLE status_public.user_achievements TO authenticated;
37
+
38
+ COMMIT;
@@ -0,0 +1,20 @@
1
+ -- Deploy schemas/status_public/tables/user_achievements/table to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+
5
+ BEGIN;
6
+
7
+ CREATE TABLE status_public.user_achievements (
8
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (),
9
+ user_id uuid NOT NULL,
10
+ name text NOT NULL, -- relates to level_requirements.name
11
+ count int NOT NULL DEFAULT 0,
12
+ created_at timestamptz NOT NULL DEFAULT current_timestamp,
13
+ constraint user_achievements_unique_key unique (user_id, name)
14
+ );
15
+
16
+ COMMENT ON TABLE status_public.user_achievements IS 'This table represents the users progress for particular level requirements, tallying the total count. This table is updated via triggers and should not be updated maually.';
17
+
18
+ CREATE INDEX ON status_public.user_achievements (user_id, name);
19
+
20
+ COMMIT;
@@ -0,0 +1,20 @@
1
+ -- Deploy schemas/status_public/tables/user_levels/table to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+
5
+ BEGIN;
6
+
7
+ -- NOT using yet, so commented it out for simplicity
8
+
9
+ -- CREATE TABLE status_public.user_levels (
10
+ -- id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (),
11
+ -- user_id uuid NOT NULL,
12
+ -- name text NOT NULL, -- references levels
13
+ -- created_at timestamptz NOT NULL DEFAULT current_timestamp
14
+ -- );
15
+
16
+ -- COMMENT ON TABLE status_public.user_levels IS 'Cache table of the achieved levels';
17
+
18
+ -- CREATE INDEX ON status_public.user_levels (user_id, name);
19
+
20
+ COMMIT;
@@ -0,0 +1,18 @@
1
+ -- Deploy schemas/status_public/tables/user_steps/table to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+
5
+ BEGIN;
6
+
7
+ CREATE TABLE status_public.user_steps (
8
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (),
9
+ user_id uuid NOT NULL,
10
+ name text NOT NULL, -- references level_requirement
11
+ count int NOT NULL DEFAULT 1,
12
+ created_at timestamptz NOT NULL DEFAULT current_timestamp
13
+ );
14
+
15
+ COMMENT ON TABLE status_public.user_steps IS 'The user achieving a requirement for a level. Log table that has every single step ever taken.';
16
+ CREATE INDEX ON status_public.user_steps (user_id, name);
17
+
18
+ COMMIT;
@@ -0,0 +1,25 @@
1
+ -- Deploy schemas/status_public/tables/user_steps/triggers/update_achievements_tg to pg
2
+
3
+ -- requires: schemas/status_public/schema
4
+ -- requires: schemas/status_public/tables/user_steps/table
5
+ -- requires: schemas/status_public/tables/user_achievements/table
6
+ -- requires: schemas/status_private/procedures/upsert_achievement
7
+
8
+ BEGIN;
9
+
10
+ CREATE FUNCTION status_private.tg_update_achievements_tg()
11
+ RETURNS TRIGGER AS $$
12
+ DECLARE
13
+ BEGIN
14
+ PERFORM status_private.upsert_achievement(NEW.user_id, NEW.name, NEW.count);
15
+ RETURN NEW;
16
+ END;
17
+ $$
18
+ LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER;
19
+
20
+ CREATE TRIGGER update_achievements_tg
21
+ AFTER INSERT ON status_public.user_steps
22
+ FOR EACH ROW
23
+ EXECUTE PROCEDURE status_private.tg_update_achievements_tg ();
24
+
25
+ COMMIT;