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

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