Skip to main content

systemprompt_content/repository/link/
mod.rs

1pub mod analytics;
2
3pub use analytics::LinkAnalyticsRepository;
4
5use crate::error::ContentError;
6use crate::models::{CampaignLink, CreateLinkParams};
7use chrono::Utc;
8use sqlx::PgPool;
9use std::sync::Arc;
10use systemprompt_database::DbPool;
11use systemprompt_identifiers::{CampaignId, ContentId, LinkId};
12
13#[derive(Debug)]
14pub struct LinkRepository {
15    pool: Arc<PgPool>,
16}
17
18impl LinkRepository {
19    pub fn new(db: &DbPool) -> Result<Self, ContentError> {
20        let pool = db
21            .pool_arc()
22            .map_err(|e| ContentError::InvalidRequest(format!("Database pool error: {e}")))?;
23        Ok(Self { pool })
24    }
25
26    #[allow(clippy::cognitive_complexity)]
27    pub async fn create_link(
28        &self,
29        params: &CreateLinkParams,
30    ) -> Result<CampaignLink, sqlx::Error> {
31        let id = LinkId::generate();
32        let now = Utc::now();
33        sqlx::query_as!(
34            CampaignLink,
35            r#"
36            INSERT INTO campaign_links (
37                id, short_code, target_url, link_type, source_content_id, source_page,
38                campaign_id, campaign_name, utm_params, link_text, link_position,
39                destination_type, is_active, expires_at, created_at, updated_at
40            )
41            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $15)
42            RETURNING id as "id: LinkId", short_code, target_url, link_type,
43                      campaign_id as "campaign_id: CampaignId", campaign_name,
44                      source_content_id as "source_content_id: ContentId", source_page,
45                      utm_params, link_text, link_position, destination_type,
46                      click_count, unique_click_count, conversion_count,
47                      is_active, expires_at, created_at, updated_at
48            "#,
49            id.as_str(),
50            params.short_code,
51            params.target_url,
52            params.link_type,
53            params.source_content_id.as_ref().map(ContentId::as_str),
54            params.source_page,
55            params.campaign_id.as_ref().map(CampaignId::as_str),
56            params.campaign_name,
57            params.utm_params,
58            params.link_text,
59            params.link_position,
60            params.destination_type,
61            params.is_active,
62            params.expires_at,
63            now
64        )
65        .fetch_one(&*self.pool)
66        .await
67    }
68
69    pub async fn get_link_by_short_code(
70        &self,
71        short_code: &str,
72    ) -> Result<Option<CampaignLink>, sqlx::Error> {
73        sqlx::query_as!(
74            CampaignLink,
75            r#"
76            SELECT id as "id: LinkId", short_code, target_url, link_type,
77                   campaign_id as "campaign_id: CampaignId", campaign_name,
78                   source_content_id as "source_content_id: ContentId", source_page,
79                   utm_params, link_text, link_position, destination_type,
80                   click_count, unique_click_count, conversion_count,
81                   is_active, expires_at, created_at, updated_at
82            FROM campaign_links
83            WHERE short_code = $1 AND is_active = true
84            "#,
85            short_code
86        )
87        .fetch_optional(&*self.pool)
88        .await
89    }
90
91    pub async fn list_links_by_campaign(
92        &self,
93        campaign_id: &CampaignId,
94    ) -> Result<Vec<CampaignLink>, sqlx::Error> {
95        sqlx::query_as!(
96            CampaignLink,
97            r#"
98            SELECT id as "id: LinkId", short_code, target_url, link_type,
99                   campaign_id as "campaign_id: CampaignId", campaign_name,
100                   source_content_id as "source_content_id: ContentId", source_page,
101                   utm_params, link_text, link_position, destination_type,
102                   click_count, unique_click_count, conversion_count,
103                   is_active, expires_at, created_at, updated_at
104            FROM campaign_links
105            WHERE campaign_id = $1
106            ORDER BY created_at DESC
107            "#,
108            campaign_id.as_str()
109        )
110        .fetch_all(&*self.pool)
111        .await
112    }
113
114    pub async fn list_links_by_source_content(
115        &self,
116        content_id: &ContentId,
117    ) -> Result<Vec<CampaignLink>, sqlx::Error> {
118        sqlx::query_as!(
119            CampaignLink,
120            r#"
121            SELECT id as "id: LinkId", short_code, target_url, link_type,
122                   campaign_id as "campaign_id: CampaignId", campaign_name,
123                   source_content_id as "source_content_id: ContentId", source_page,
124                   utm_params, link_text, link_position, destination_type,
125                   click_count, unique_click_count, conversion_count,
126                   is_active, expires_at, created_at, updated_at
127            FROM campaign_links
128            WHERE source_content_id = $1
129            ORDER BY created_at DESC
130            "#,
131            content_id.as_str()
132        )
133        .fetch_all(&*self.pool)
134        .await
135    }
136
137    pub async fn get_link_by_id(&self, id: &LinkId) -> Result<Option<CampaignLink>, sqlx::Error> {
138        sqlx::query_as!(
139            CampaignLink,
140            r#"
141            SELECT id as "id: LinkId", short_code, target_url, link_type,
142                   campaign_id as "campaign_id: CampaignId", campaign_name,
143                   source_content_id as "source_content_id: ContentId", source_page,
144                   utm_params, link_text, link_position, destination_type,
145                   click_count, unique_click_count, conversion_count,
146                   is_active, expires_at, created_at, updated_at
147            FROM campaign_links
148            WHERE id = $1
149            "#,
150            id.as_str()
151        )
152        .fetch_optional(&*self.pool)
153        .await
154    }
155
156    pub async fn delete_link(&self, id: &LinkId) -> Result<bool, sqlx::Error> {
157        let result = sqlx::query!("DELETE FROM campaign_links WHERE id = $1", id.as_str())
158            .execute(&*self.pool)
159            .await?;
160        Ok(result.rows_affected() > 0)
161    }
162}