omni_orchestrator/schemas/v1/db/queries/
app.rs

1use crate::models::app::{App, AppWithInstanceCount, AppWithInstances};
2use crate::models::instance::Instance;
3use anyhow::Context;
4use serde::Serialize;
5use sqlx::{MySql, Pool};
6
7/// Retrieves a paginated list of applications from the database.
8///
9/// This function fetches a subset of applications based on pagination parameters,
10/// ordering them by their ID in ascending order. Pagination helps manage large
11/// datasets by retrieving only a specific "page" of results.
12///
13/// # Arguments
14///
15/// * `pool` - Database connection pool for executing the query
16/// * `page` - Zero-based page number (e.g., 0 for first page, 1 for second page)
17/// * `per_page` - Number of records to fetch per page
18///
19/// # Returns
20///
21/// * `Ok(Vec<App>)` - Successfully retrieved list of applications
22/// * `Err(anyhow::Error)` - Failed to fetch applications, with context
23///
24/// # Examples
25///
26/// ```
27/// let apps = list_apps(&pool, 0, 10).await?; // Get first 10 apps
28/// ```
29pub async fn list_apps(pool: &Pool<MySql>, page: i64, per_page: i64) -> anyhow::Result<Vec<AppWithInstanceCount>> {
30    println!("Attempting to fetch apps with instance counts from database...");
31
32    let result = sqlx::query_as::<_, AppWithInstanceCount>(
33        r#"
34        SELECT 
35            apps.*, 
36            COUNT(instances.id) AS instance_count
37        FROM 
38            apps
39        LEFT JOIN 
40            instances ON instances.app_id = apps.id
41        GROUP BY 
42            apps.id
43        ORDER BY 
44            apps.id ASC
45        LIMIT ? OFFSET ?
46        "#,
47    )
48    .bind(per_page)
49    .bind(page * per_page)
50    .fetch_all(pool)
51    .await;
52
53    match result {
54        Ok(apps) => {
55            println!("Successfully fetched {} apps with instance counts", apps.len());
56            Ok(apps)
57        }
58        Err(e) => {
59            eprintln!("Error fetching apps with instance counts: {:#?}", e);
60            Err(anyhow::Error::new(e).context("Failed to fetch apps with instance counts"))
61        }
62    }
63}
64
65/// Retrieves a specific application along with its associated instances.
66/// 
67/// This function fetches an application by its ID and also retrieves all instances
68/// associated with that application. The results are combined into a single structure
69/// for easier access.
70/// 
71/// # Arguments
72/// 
73/// * `pool` - Database connection pool for executing the query
74/// * `app_id` - Unique identifier of the application to retrieve
75/// 
76/// # Returns
77/// 
78/// * `Ok(AppWithInstances)` - Successfully retrieved application and its instances
79/// * `Err(anyhow::Error)` - Failed to fetch application or instances, with context
80pub async fn get_app_with_instances(pool: &Pool<MySql>, app_id: i64) -> anyhow::Result<AppWithInstances> {
81    // First fetch the app data
82    let app = sqlx::query_as::<_, App>(
83        "SELECT * FROM apps WHERE id = ?",
84    )
85    .bind(app_id)
86    .fetch_one(pool)
87    .await
88    .context("Failed to fetch app")?;
89    
90    // Then fetch all instances for this app
91    let instances = sqlx::query_as::<_, Instance>(
92        "SELECT * FROM instances WHERE app_id = ? ORDER BY created_at DESC",
93    )
94    .bind(app_id)
95    .fetch_all(pool)
96    .await
97    .context("Failed to fetch instances")?;
98    
99    // Combine into the AppWithInstances structure
100    Ok(AppWithInstances {
101        app,
102        instances,
103    })
104}
105
106/// Counts the total number of applications in the database.
107///
108/// This function retrieves the total count of applications, which can be useful
109/// for pagination or reporting purposes.
110pub async fn count_apps(pool: &Pool<MySql>) -> anyhow::Result<i64> {
111    let count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM apps")
112        .fetch_one(pool)
113        .await
114        .context("Failed to count apps")?;
115
116    Ok(count)
117}
118
119/// Retrieves a specific application by its unique identifier.
120///
121/// This function fetches a single application record matching the provided ID.
122/// It's typically used for retrieving detailed information about a specific application.
123///
124/// # Arguments
125///
126/// * `pool` - Database connection pool for executing the query
127/// * `id` - Unique identifier of the application to retrieve
128///
129/// # Returns
130///
131/// * `Ok(App)` - Successfully retrieved application
132/// * `Err(anyhow::Error)` - Failed to fetch application (including if not found)
133///
134/// # Error Handling
135///
136/// Returns an error if no application with the given ID exists or if a database
137/// error occurs during the query execution.
138pub async fn get_app_by_id(pool: &Pool<MySql>, id: i64) -> anyhow::Result<App> {
139    let app = sqlx::query_as::<_, App>("SELECT * FROM apps WHERE id = ?")
140        .bind(id)
141        .fetch_one(pool)
142        .await
143        .context("Failed to fetch app")?;
144
145    Ok(app)
146}
147
148/// Retrieves all applications belonging to a specific organization.
149///
150/// This function fetches all applications associated with the provided organization ID,
151/// ordered by creation date in descending order (newest first). It's typically used
152/// to display all applications owned by an organization.
153///
154/// # Arguments
155///
156/// * `pool` - Database connection pool for executing the query
157/// * `org_id` - Organization identifier to filter applications by
158///
159/// # Returns
160///
161/// * `Ok(Vec<App>)` - Successfully retrieved list of applications for the organization
162/// * `Err(anyhow::Error)` - Failed to fetch applications
163///
164/// # Note
165///
166/// This function will return an empty vector if the organization exists but has no applications.
167pub async fn get_apps_by_org(pool: &Pool<MySql>, org_id: i64) -> anyhow::Result<Vec<App>> {
168    let apps =
169        sqlx::query_as::<_, App>("SELECT * FROM apps WHERE org_id = ? ORDER BY created_at DESC")
170            .bind(org_id)
171            .fetch_all(pool)
172            .await
173            .context("Failed to fetch org apps")?;
174
175    Ok(apps)
176}
177
178/// Creates a new application in the database.
179///
180/// This function inserts a new application record with the provided parameters.
181/// It handles both required fields (name, organization ID) and optional fields.
182/// The application is created with maintenance mode disabled by default.
183///
184/// # Arguments
185///
186/// * `pool` - Database connection pool for executing the query
187/// * `name` - Name of the application
188/// * `org_id` - Organization ID that the application belongs to
189/// * `git_repo` - Optional URL of the Git repository for the application
190/// * `git_branch` - Optional branch name in the Git repository
191/// * `container_image_url` - Optional URL for a container image
192/// * `region_id` - Optional ID of the deployment region
193///
194/// # Returns
195///
196/// * `Ok(App)` - Successfully created application, including database-assigned fields
197/// * `Err(anyhow::Error)` - Failed to create application
198///
199/// # Transaction Handling
200///
201/// This function uses a database transaction to ensure atomicity of the operation.
202/// If any part of the operation fails, the entire operation is rolled back.
203pub async fn create_app(
204    pool: &Pool<MySql>,
205    name: &str,
206    org_id: i64,
207    git_repo: Option<&str>,
208    git_branch: Option<&str>,
209    container_image_url: Option<&str>,
210    region_id: Option<i64>,
211) -> anyhow::Result<App> {
212    // Begin transaction
213    let mut tx = pool.begin().await?;
214
215    // Define query to insert app with default maintenance_mode set to false
216    let app = sqlx::query_as::<_, App>(
217        r#"INSERT INTO apps (
218            name, org_id, git_repo, git_branch, container_image_url, region_id, maintenance_mode
219        ) VALUES (?, ?, ?, ?, ?, ?, false)"#,
220    )
221    // Bind required parameters
222    .bind(name)
223    .bind(org_id)
224    // Bind optional parameters
225    .bind(git_repo)
226    .bind(git_branch)
227    .bind(container_image_url)
228    .bind(region_id)
229    // Execute query and handle errors
230    .fetch_one(&mut *tx)
231    .await
232    .context("Failed to create app")?;
233
234    // Commit transaction
235    tx.commit().await?;
236
237    // Return newly created app
238    Ok(app)
239}
240
241/// Updates an existing application in the database.
242///
243/// This function modifies an application record with the provided parameters.
244/// It uses a dynamic SQL query that only updates fields for which values are provided,
245/// leaving other fields unchanged. The updated_at timestamp is always updated
246/// to reflect the modification time.
247///
248/// # Arguments
249///
250/// * `pool` - Database connection pool for executing the query
251/// * `id` - Unique identifier of the application to update
252/// * `name` - Optional new name for the application
253/// * `git_repo` - Optional new Git repository URL
254/// * `git_branch` - Optional new Git branch
255/// * `container_image_url` - Optional new container image URL
256/// * `region_id` - Optional new region ID
257/// * `maintenance_mode` - Optional new maintenance mode status
258///
259/// # Returns
260///
261/// * `Ok(App)` - Successfully updated application with all current values
262/// * `Err(anyhow::Error)` - Failed to update application
263///
264/// # Dynamic Query Building
265///
266/// The function dynamically constructs the UPDATE SQL statement based on which
267/// parameters have values. This ensures the query only updates the fields that
268/// need to change, improving efficiency and reducing the risk of unintended changes.
269///
270/// # Transaction Handling
271///
272/// This function uses a database transaction to ensure atomicity of the operation.
273/// If any part of the operation fails, the entire operation is rolled back.
274pub async fn update_app(
275    pool: &Pool<MySql>,
276    id: i64,
277    name: Option<&str>,
278    git_repo: Option<&str>,
279    git_branch: Option<&str>,
280    container_image_url: Option<&str>,
281    region_id: Option<i64>,
282    maintenance_mode: Option<bool>,
283) -> anyhow::Result<App> {
284    // Define which fields are being updated
285    let update_fields = [
286        (name.is_some(), "name = ?"),
287        (git_repo.is_some(), "git_repo = ?"),
288        (git_branch.is_some(), "git_branch = ?"),
289        (container_image_url.is_some(), "container_image_url = ?"),
290        (region_id.is_some(), "region_id = ?"),
291        (maintenance_mode.is_some(), "maintenance_mode = ?"),
292    ];
293
294    // Build update query with only the fields that have values
295    let field_clauses = update_fields
296        .iter()
297        .filter(|(has_value, _)| *has_value)
298        .map(|(_, field)| format!(", {}", field))
299        .collect::<String>();
300
301    let query = format!(
302        "UPDATE apps SET updated_at = CURRENT_TIMESTAMP{} WHERE id = ?",
303        field_clauses
304    );
305
306    // Start binding parameters
307    let mut db_query = sqlx::query_as::<_, App>(&query);
308
309    // Bind string parameters
310    if let Some(val) = name {
311        db_query = db_query.bind(val);
312    }
313    if let Some(val) = git_repo {
314        db_query = db_query.bind(val);
315    }
316    if let Some(val) = git_branch {
317        db_query = db_query.bind(val);
318    }
319    if let Some(val) = container_image_url {
320        db_query = db_query.bind(val);
321    }
322
323    // Bind numeric/boolean parameters
324    if let Some(val) = region_id {
325        db_query = db_query.bind(val);
326    }
327    if let Some(val) = maintenance_mode {
328        db_query = db_query.bind(val);
329    }
330
331    // Bind the ID parameter
332    db_query = db_query.bind(id);
333
334    // Execute the query in a transaction
335    let mut tx = pool.begin().await?;
336    let app = db_query
337        .fetch_one(&mut *tx)
338        .await
339        .context("Failed to update app")?;
340
341    tx.commit().await?;
342    Ok(app)
343}
344
345/// Deletes an application from the database.
346///
347/// This function permanently removes an application record with the specified ID.
348/// The operation is performed within a transaction to ensure data consistency.
349///
350/// # Arguments
351///
352/// * `pool` - Database connection pool for executing the query
353/// * `id` - Unique identifier of the application to delete
354///
355/// # Returns
356///
357/// * `Ok(())` - Successfully deleted the application
358/// * `Err(anyhow::Error)` - Failed to delete the application
359///
360/// # Warning
361///
362/// This operation is irreversible. Once an application is deleted, all associated
363/// data that depends on the application's existence may become invalid.
364///
365/// # Note
366///
367/// This function does not verify if the application exists before attempting deletion.
368/// If the application does not exist, the operation will still succeed (as far as SQL is concerned),
369/// but no rows will be affected.
370pub async fn delete_app(pool: &Pool<MySql>, id: i64) -> anyhow::Result<()> {
371    let mut tx = pool.begin().await?;
372
373    sqlx::query("DELETE FROM apps WHERE id = ?")
374        .bind(id)
375        .execute(&mut *tx)
376        .await
377        .context("Failed to delete app")?;
378
379    tx.commit().await?;
380    Ok(())
381}
382
383/// Sets the maintenance mode status for an application.
384///
385/// This function updates only the maintenance_mode field of an application,
386/// making it a more efficient alternative to update_app when only this field 
387/// needs to change. When an application is in maintenance mode, it typically
388/// displays a maintenance page to users instead of normal operation.
389///
390/// # Arguments
391///
392/// * `pool` - Database connection pool for executing the query
393/// * `id` - Unique identifier of the application to update
394/// * `maintenance_mode` - Whether maintenance mode should be enabled (true) or disabled (false)
395///
396/// # Returns
397///
398/// * `Ok(App)` - Successfully updated application with the new maintenance mode status
399/// * `Err(anyhow::Error)` - Failed to update maintenance mode
400///
401/// # Transaction Handling
402///
403/// This function uses a database transaction to ensure atomicity of the operation.
404/// If any part of the operation fails, the entire operation is rolled back.
405pub async fn set_maintenance_mode(
406    pool: &Pool<MySql>,
407    id: i64,
408    maintenance_mode: bool,
409) -> anyhow::Result<App> {
410    let mut tx = pool.begin().await?;
411
412    let app = sqlx::query_as::<_, App>(
413        "UPDATE apps SET maintenance_mode = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
414    )
415    .bind(maintenance_mode)
416    .bind(id)
417    .fetch_one(&mut *tx)
418    .await
419    .context(format!("Failed to update app {} maintenance mode", id))?;
420
421    tx.commit().await?;
422    Ok(app)
423}
424
425/// Retrieves a paginated list of instances for a specific application.
426///
427/// This function fetches a subset of instances associated with an application,
428/// ordered by creation time with the most recent first. Pagination helps manage
429/// large datasets by retrieving only a specific "page" of results.
430///
431/// # Arguments
432///
433/// * `pool` - Database connection pool for executing the query
434/// * `app_id` - Unique identifier of the application whose instances to retrieve
435/// * `page` - Zero-based page number (e.g., 0 for first page, 1 for second page)
436/// * `per_page` - Number of records to fetch per page
437///
438/// # Returns
439///
440/// * `Ok(Vec<Instance>)` - Successfully retrieved list of instances
441/// * `Err(anyhow::Error)` - Failed to fetch instances
442///
443/// # Use Cases
444///
445/// Common use cases include:
446/// - Displaying paginated instances in an application dashboard
447/// - Monitoring resource usage across an application
448/// - Auditing instance allocation and lifecycle
449pub async fn list_instances(
450    pool: &Pool<MySql>,
451    app_id: i64,
452    page: i64,
453    per_page: i64,
454) -> anyhow::Result<Vec<Instance>> {
455    let instances = sqlx::query_as::<_, Instance>(
456        "SELECT * FROM instances WHERE app_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?",
457    )
458    .bind(app_id)
459    .bind(per_page)
460    .bind(page * per_page)
461    .fetch_all(pool)
462    .await
463    .context("Failed to fetch instances")?;
464
465    Ok(instances)
466}
467
468/// Counts the number of instances for each application (grouped by app_id).
469///
470/// This function returns a vector of tuples, where each tuple contains an app_id
471/// and the corresponding count of instances for that app. Useful for dashboards
472/// or analytics where you want to see instance distribution per app.
473pub async fn count_instances_by_app(pool: &Pool<MySql>, app_id: i64) -> anyhow::Result<i64> {
474    let count = sqlx::query_scalar::<_, i64>(
475        "SELECT COUNT(*) FROM instances where app_id = ?"
476    )
477    .bind(app_id)
478    .fetch_one(pool)
479    .await
480    .context("Failed to count instances by app_id")?;
481
482    Ok(count)
483}