From 46d07163cd1cdba6fbc0fb444f72185dd2702229 Mon Sep 17 00:00:00 2001
From: Sychic <47618543+Sychic@users.noreply.github.com>
Date: Mon, 17 Aug 2026 13:57:57 -0400
Subject: [PATCH] feat(labrinth): user preferences (#7177)
* feat(labrinth): initial preferences structure
* feat(labrinth): use partially to derive partial structs
* feat(labrinth): preference defaults
* fix(labrinth): derive debug
* feat(labrinth): user preferences db setup
* feat(labrinth): user preferences routes
* fix(labrinth): nested partial structs
* fix(labrinth): serialize enums as snake case
* refactor(labrinth-derive): rename to component-derive
* feat(component-derive): nested components
* refactor(component-derive): change suffixes to prefixes
* refactor(component-derive): rename edit to partial
* feat(component-derive): skip serializing empty option
* refactor(labrinth): wrap errors
* feat(component-derive): diff function
* style(labrinth): fmt
* refactor(labrinth): use component derive and only store overrides
* chore: update query cache
* docs(labrinth): add preferences to openapi
* fix(labrinth): lock row for update
* remove: partially
* refactor(component-derive): split impls to separate functions
* feat(component-derive): wrap impls to isolate naming
* refactor(labrinth): split out auth conditions
* feat(labrinth): store auto
* feat(labrinth): store hosting privacy
* style(labrinth): cargo fmt
---
.idea/code.iml | 1 +
Cargo.lock | 21 +-
Cargo.toml | 3 +-
...69f43909750257d6b4f86cfecc89568b86cb9.json | 22 ++
...267e8328df9846d35c4df149752098a750caa.json | 22 ++
...823cd02c215c4b219859724928ebdb99e493f.json | 15 +
apps/labrinth/Cargo.toml | 1 +
.../20260806120000_user-preferences.sql | 4 +
apps/labrinth/src/database/models/mod.rs | 1 +
.../database/models/user_preferences_item.rs | 75 +++++
apps/labrinth/src/models/v3/mod.rs | 1 +
apps/labrinth/src/models/v3/preferences.rs | 123 ++++++++
apps/labrinth/src/routes/v3/mod.rs | 2 +
apps/labrinth/src/routes/v3/users.rs | 120 ++++++++
.../Cargo.toml | 2 +-
packages/component-derive/src/component.rs | 279 ++++++++++++++++++
.../src/lib.rs | 0
packages/labrinth-derive/src/component.rs | 161 ----------
18 files changed, 680 insertions(+), 173 deletions(-)
create mode 100644 apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json
create mode 100644 apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json
create mode 100644 apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json
create mode 100644 apps/labrinth/migrations/20260806120000_user-preferences.sql
create mode 100644 apps/labrinth/src/database/models/user_preferences_item.rs
create mode 100644 apps/labrinth/src/models/v3/preferences.rs
rename packages/{labrinth-derive => component-derive}/Cargo.toml (91%)
create mode 100644 packages/component-derive/src/component.rs
rename packages/{labrinth-derive => component-derive}/src/lib.rs (100%)
delete mode 100644 packages/labrinth-derive/src/component.rs
diff --git a/.idea/code.iml b/.idea/code.iml
index 3861923c60..a1d7c854b4 100644
--- a/.idea/code.iml
+++ b/.idea/code.iml
@@ -28,6 +28,7 @@
+
diff --git a/Cargo.lock b/Cargo.lock
index 0772a3a9c1..89ee75a3b7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2223,6 +2223,16 @@ dependencies = [
"static_assertions",
]
+[[package]]
+name = "component-derive"
+version = "0.0.0"
+dependencies = [
+ "darling 0.23.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
[[package]]
name = "compression-codecs"
version = "0.4.31"
@@ -5480,6 +5490,7 @@ dependencies = [
"clickhouse",
"color-eyre",
"color-thief",
+ "component-derive",
"const_format",
"dashmap",
"derive_more 2.1.1",
@@ -5557,16 +5568,6 @@ dependencies = [
"zxcvbn",
]
-[[package]]
-name = "labrinth-derive"
-version = "0.0.0"
-dependencies = [
- "darling 0.23.0",
- "proc-macro2",
- "quote",
- "syn 2.0.106",
-]
-
[[package]]
name = "language-tags"
version = "0.3.2"
diff --git a/Cargo.toml b/Cargo.toml
index ab510f52d7..783f73631a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,8 +7,8 @@ members = [
"apps/labrinth",
"packages/app-lib",
"packages/ariadne",
+ "packages/component-derive",
"packages/daedalus",
- "packages/labrinth-derive",
"packages/modrinth-content-management",
"packages/modrinth-log",
"packages/modrinth-maxmind",
@@ -68,6 +68,7 @@ clap = "4.5.48"
clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
+component-derive = { path = "packages/component-derive" }
const_format = "0.2.34"
core-foundation = "0.10.1"
core-graphics = "0.24.0"
diff --git a/apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json b/apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json
new file mode 100644
index 0000000000..7aa05f7bab
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT preferences AS \"preferences: Json\"\n FROM user_preferences\n WHERE user_id = $1\n FOR UPDATE\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "preferences: Json",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9"
+}
diff --git a/apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json b/apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json
new file mode 100644
index 0000000000..431861125a
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT preferences AS \"preferences: Json\"\n FROM user_preferences\n WHERE user_id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "preferences: Json",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa"
+}
diff --git a/apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json b/apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json
new file mode 100644
index 0000000000..d27a0354eb
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO user_preferences (user_id, preferences)\n VALUES ($1, $2)\n ON CONFLICT (user_id) DO UPDATE\n SET preferences = EXCLUDED.preferences\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Jsonb"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f"
+}
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index fbfe31b0bf..6c3ab65446 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -40,6 +40,7 @@ clap = { workspace = true, features = ["derive"] }
clickhouse = { workspace = true, features = ["time", "uuid"] }
color-eyre = { workspace = true }
color-thief = { workspace = true }
+component-derive = { workspace = true }
const_format = { workspace = true }
dashmap = { workspace = true }
derive_more = { workspace = true, features = ["deref", "deref_mut"] }
diff --git a/apps/labrinth/migrations/20260806120000_user-preferences.sql b/apps/labrinth/migrations/20260806120000_user-preferences.sql
new file mode 100644
index 0000000000..ed9409d52b
--- /dev/null
+++ b/apps/labrinth/migrations/20260806120000_user-preferences.sql
@@ -0,0 +1,4 @@
+CREATE TABLE user_preferences (
+ user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
+ preferences JSONB NOT NULL
+);
diff --git a/apps/labrinth/src/database/models/mod.rs b/apps/labrinth/src/database/models/mod.rs
index 896791e7d3..e647428669 100644
--- a/apps/labrinth/src/database/models/mod.rs
+++ b/apps/labrinth/src/database/models/mod.rs
@@ -38,6 +38,7 @@ pub mod team_item;
pub mod thread_item;
pub mod user_item;
pub mod user_limits;
+pub mod user_preferences_item;
pub mod user_subscription_item;
pub mod users_compliance;
pub mod users_notifications_preferences_item;
diff --git a/apps/labrinth/src/database/models/user_preferences_item.rs b/apps/labrinth/src/database/models/user_preferences_item.rs
new file mode 100644
index 0000000000..853a6397e2
--- /dev/null
+++ b/apps/labrinth/src/database/models/user_preferences_item.rs
@@ -0,0 +1,75 @@
+use crate::database::Executor;
+use crate::database::models::DBUserId;
+use crate::models::v3::preferences::PartialUserPreferences;
+use sqlx::types::Json;
+
+pub struct DBUserPreferences;
+
+impl DBUserPreferences {
+ pub async fn get<'a, E>(
+ user_id: DBUserId,
+ exec: E,
+ ) -> Result