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}