Skip to main content

mini_apm/models/
project.rs

1use crate::DbPool;
2use chrono::Utc;
3use rand::Rng;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Project {
8    pub id: i64,
9    pub name: String,
10    pub slug: String,
11    pub api_key: String,
12    pub created_at: String,
13}
14
15/// Generate a random API key for a project
16fn generate_api_key() -> String {
17    let mut rng = rand::thread_rng();
18    let bytes: [u8; 24] = rng.r#gen();
19    format!("proj_{}", hex::encode(bytes))
20}
21
22/// Generate a slug from project name
23fn slugify(name: &str) -> String {
24    name.to_lowercase()
25        .chars()
26        .map(|c| if c.is_alphanumeric() { c } else { '-' })
27        .collect::<String>()
28        .split('-')
29        .filter(|s| !s.is_empty())
30        .collect::<Vec<_>>()
31        .join("-")
32}
33
34/// Ensure default project exists when projects are enabled
35pub fn ensure_default_project(pool: &DbPool) -> anyhow::Result<Project> {
36    let conn = pool.get()?;
37
38    // Check if any project exists
39    let count: i64 = conn.query_row("SELECT COUNT(*) FROM projects", [], |row| row.get(0))?;
40
41    if count == 0 {
42        let now = Utc::now().to_rfc3339();
43        let api_key = generate_api_key();
44
45        conn.execute(
46            "INSERT INTO projects (name, slug, api_key, created_at) VALUES (?1, ?2, ?3, ?4)",
47            ("Default", "default", &api_key, &now),
48        )?;
49
50        tracing::info!("Created default project with API key: {}", api_key);
51
52        return Ok(Project {
53            id: conn.last_insert_rowid(),
54            name: "Default".to_string(),
55            slug: "default".to_string(),
56            api_key,
57            created_at: now,
58        });
59    }
60
61    // Return first project
62    let project = conn.query_row(
63        "SELECT id, name, slug, api_key, created_at FROM projects ORDER BY id LIMIT 1",
64        [],
65        |row| {
66            Ok(Project {
67                id: row.get(0)?,
68                name: row.get(1)?,
69                slug: row.get(2)?,
70                api_key: row.get(3)?,
71                created_at: row.get(4)?,
72            })
73        },
74    )?;
75
76    Ok(project)
77}
78
79/// List all projects
80pub fn list_all(pool: &DbPool) -> anyhow::Result<Vec<Project>> {
81    let conn = pool.get()?;
82    let mut stmt = conn.prepare(
83        "SELECT id, name, slug, api_key, strftime('%Y-%m-%d %H:%M', created_at) FROM projects ORDER BY name",
84    )?;
85
86    let projects = stmt
87        .query_map([], |row| {
88            Ok(Project {
89                id: row.get(0)?,
90                name: row.get(1)?,
91                slug: row.get(2)?,
92                api_key: row.get(3)?,
93                created_at: row.get(4)?,
94            })
95        })?
96        .collect::<Result<Vec<_>, _>>()?;
97
98    Ok(projects)
99}
100
101/// Find project by ID
102pub fn find(pool: &DbPool, id: i64) -> anyhow::Result<Option<Project>> {
103    let conn = pool.get()?;
104
105    let project = conn
106        .query_row(
107            "SELECT id, name, slug, api_key, created_at FROM projects WHERE id = ?1",
108            [id],
109            |row| {
110                Ok(Project {
111                    id: row.get(0)?,
112                    name: row.get(1)?,
113                    slug: row.get(2)?,
114                    api_key: row.get(3)?,
115                    created_at: row.get(4)?,
116                })
117            },
118        )
119        .ok();
120
121    Ok(project)
122}
123
124/// Find project by slug
125pub fn find_by_slug(pool: &DbPool, slug: &str) -> anyhow::Result<Option<Project>> {
126    let conn = pool.get()?;
127
128    let project = conn
129        .query_row(
130            "SELECT id, name, slug, api_key, created_at FROM projects WHERE slug = ?1",
131            [slug],
132            |row| {
133                Ok(Project {
134                    id: row.get(0)?,
135                    name: row.get(1)?,
136                    slug: row.get(2)?,
137                    api_key: row.get(3)?,
138                    created_at: row.get(4)?,
139                })
140            },
141        )
142        .ok();
143
144    Ok(project)
145}
146
147/// Find project by API key
148pub fn find_by_api_key(pool: &DbPool, api_key: &str) -> anyhow::Result<Option<Project>> {
149    let conn = pool.get()?;
150
151    let project = conn
152        .query_row(
153            "SELECT id, name, slug, api_key, created_at FROM projects WHERE api_key = ?1",
154            [api_key],
155            |row| {
156                Ok(Project {
157                    id: row.get(0)?,
158                    name: row.get(1)?,
159                    slug: row.get(2)?,
160                    api_key: row.get(3)?,
161                    created_at: row.get(4)?,
162                })
163            },
164        )
165        .ok();
166
167    Ok(project)
168}
169
170/// Create a new project
171pub fn create(pool: &DbPool, name: &str) -> anyhow::Result<Project> {
172    let conn = pool.get()?;
173
174    let now = Utc::now().to_rfc3339();
175    let slug = slugify(name);
176    let api_key = generate_api_key();
177
178    conn.execute(
179        "INSERT INTO projects (name, slug, api_key, created_at) VALUES (?1, ?2, ?3, ?4)",
180        (name, &slug, &api_key, &now),
181    )?;
182
183    let project_id = conn.last_insert_rowid();
184
185    Ok(Project {
186        id: project_id,
187        name: name.to_string(),
188        slug,
189        api_key,
190        created_at: now,
191    })
192}
193
194/// Delete a project
195pub fn delete(pool: &DbPool, id: i64) -> anyhow::Result<()> {
196    let conn = pool.get()?;
197    conn.execute("DELETE FROM projects WHERE id = ?1", [id])?;
198    Ok(())
199}
200
201/// Regenerate API key for a project
202pub fn regenerate_api_key(pool: &DbPool, id: i64) -> anyhow::Result<String> {
203    let conn = pool.get()?;
204    let new_key = generate_api_key();
205
206    conn.execute(
207        "UPDATE projects SET api_key = ?1 WHERE id = ?2",
208        (&new_key, id),
209    )?;
210
211    Ok(new_key)
212}
213
214/// Get project count
215pub fn count(pool: &DbPool) -> anyhow::Result<i64> {
216    let conn = pool.get()?;
217    let count: i64 = conn.query_row("SELECT COUNT(*) FROM projects", [], |row| row.get(0))?;
218    Ok(count)
219}