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}