feat(labrinth): project disclosures database model

This commit is contained in:
sychic
2026-07-31 13:34:26 +02:00
committed by Michael H.
parent a2ca0dda5a
commit f3b79b3cfc
11 changed files with 397 additions and 0 deletions
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, support)\n\t\t\tVALUES ($1, $2, $3, $4, $5)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tsupport = $5\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Jsonb",
"Int8",
"Bool"
]
},
"nullable": []
},
"hash": "02ef27605b8d19c35cebe27d618b690a420f7529a6b319893bd219760a258926"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, moderator)\n\t\t\tVALUES ($1, $2, $3, $4, $5)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tmoderator = $5\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Jsonb",
"Int8",
"Bool"
]
},
"nullable": []
},
"hash": "67443fdd9d8daaecc26e198cb439d5e5ec33d41b9ae3be81330facedd50c7564"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tDELETE FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = $2\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "9eea063cb153da407548f957cef7da1a33a059b0a1d00b51e27b4a5399492b13"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator)\n\t\t\tVALUES ($1, $2, $3, $4, $5)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tset_by_moderator = $5\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Jsonb",
"Int8",
"Bool"
]
},
"nullable": []
},
"hash": "b6b9b0cd345eddb4c4a9daf701dd0d2e5a708e59b897961f00164a28ab6294a4"
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, moderator\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "disclosure_type!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "metadata",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_by",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "moderator",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "e6db9cff0b164399dba86a54ac512813c1374a86daf5737f14fe49944de5d73f"
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, set_by_moderator\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "disclosure_type!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "metadata",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_by",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "set_by_moderator",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "ef602037ceb20dbce5313e4a90c16fb59fbbe0adf002d21fc40ca0ddd9d6ab10"
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, support\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "disclosure_type!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "metadata",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_by",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "support",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "fd137835f703a2e4041554b36c94e4499ecb9ec4ee80308bfe7fa3082be08f0c"
}
@@ -0,0 +1,9 @@
CREATE TABLE project_disclosures (
project_id BIGINT NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
type TEXT NOT NULL,
metadata JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
set_by_moderator BOOLEAN NOT NULL,
PRIMARY KEY (project_id, type)
);
+2
View File
@@ -30,6 +30,7 @@ pub mod payout_item;
pub mod payouts_values_notifications;
pub mod product_item;
pub mod products_tax_identifier_item;
pub mod project_disclosure_item;
pub mod project_item;
pub mod report_item;
pub mod session_item;
@@ -53,6 +54,7 @@ pub use image_item::DBImage;
pub use oauth_client_item::DBOAuthClient;
pub use organization_item::DBOrganization;
pub use passkey_item::DBPasskey;
pub use project_disclosure_item::DBProjectDisclosure;
pub use project_item::DBProject;
pub use team_item::DBTeam;
pub use team_item::DBTeamMember;
@@ -0,0 +1,107 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{
database::models::{DBProjectId, DBUserId, DatabaseError},
models::v3::disclosures::ProjectDisclosure,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DBProjectDisclosure {
pub project_id: DBProjectId,
pub disclosure: ProjectDisclosure,
pub updated_at: DateTime<Utc>,
pub updated_by: DBUserId,
pub set_by_moderator: bool,
}
impl DBProjectDisclosure {
pub async fn upsert(
&self,
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
let (disclosure_type, metadata) =
self.disclosure.to_parts().map_err(|e| {
DatabaseError::Internal(eyre::Report::new(e).wrap_err(
"failed to serialize project disclosure metadata",
))
})?;
sqlx::query!(
r#"
INSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (project_id, type) DO UPDATE SET
metadata = $3,
updated_at = now(),
updated_by = $4,
set_by_moderator = $5
"#,
self.project_id as DBProjectId,
disclosure_type,
metadata,
self.updated_by as DBUserId,
self.set_by_moderator,
)
.execute(exec)
.await?;
Ok(())
}
pub async fn get_many_for_project(
project_id: DBProjectId,
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<DBProjectDisclosure>, DatabaseError> {
let rows = sqlx::query!(
r#"
SELECT project_id, type AS "disclosure_type!", metadata, updated_at, updated_by, set_by_moderator
FROM project_disclosures
WHERE project_id = $1
ORDER BY updated_at DESC
"#,
project_id as DBProjectId,
)
.fetch_all(exec)
.await?;
rows.into_iter()
.map(|row| {
Ok(DBProjectDisclosure {
project_id: DBProjectId(row.project_id),
disclosure: ProjectDisclosure::from_parts(
&row.disclosure_type,
row.metadata,
)
.map_err(|e| {
DatabaseError::Internal(eyre::Report::new(e).wrap_err(
"failed to deserialize project disclosure metadata",
))
})?,
updated_at: row.updated_at,
updated_by: DBUserId(row.updated_by),
set_by_moderator: row.set_by_moderator,
})
})
.collect()
}
pub async fn remove(
project_id: DBProjectId,
disclosure_type: &str,
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
) -> Result<bool, DatabaseError> {
let result = sqlx::query!(
r#"
DELETE FROM project_disclosures
WHERE project_id = $1 AND type = $2
"#,
project_id as DBProjectId,
disclosure_type,
)
.execute(exec)
.await?;
Ok(result.rows_affected() > 0)
}
}
@@ -1,6 +1,9 @@
use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use strum::IntoStaticStr;
use utoipa::ToSchema;
use crate::database::models::DBProjectDisclosure;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoStaticStr)]
#[serde(rename_all = "snake_case", tag = "type")]
@@ -30,6 +33,57 @@ pub enum ProjectDisclosure {
},
}
impl ProjectDisclosure {
pub fn to_parts(
&self,
) -> Result<(&'static str, serde_json::Value), serde_json::Error> {
let serde_json::Value::Object(mut object) = serde_json::to_value(self)?
else {
return Err(serde::ser::Error::custom(
"project disclosure must serialize to a JSON object",
));
};
object.remove("type");
Ok((self.into(), serde_json::Value::Object(object)))
}
pub fn from_parts(
kind: &str,
metadata: serde_json::Value,
) -> Result<Self, serde_json::Error> {
let serde_json::Value::Object(mut object) = metadata else {
return Err(serde::ser::Error::custom(
"project disclosure metadata must be a JSON object, this should never be reachable",
));
};
object.insert(
"type".to_owned(),
serde_json::Value::String(kind.to_owned()),
);
serde_json::from_value(serde_json::Value::Object(object))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ProjectDisclosureData {
#[serde(flatten)]
pub disclosure: ProjectDisclosure,
pub set_by_moderator: bool,
pub updated_at: DateTime<Utc>,
pub updated_by: UserId,
}
impl From<DBProjectDisclosure> for ProjectDisclosureData {
fn from(db: DBProjectDisclosure) -> Self {
Self {
disclosure: db.disclosure,
set_by_moderator: db.set_by_moderator,
updated_at: db.updated_at,
updated_by: db.updated_by.into()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum TelementryConsent {