scope_repo.rs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. //! `scopes` catalog repository (runtime-managed, starts empty).
  2. //!
  3. //! Single source of truth for `scopes_supported`; populated only via
  4. //! `config sync` / `scope create`, never by migrations. A scope row may only be
  5. //! deleted while no `user_scopes` entitlement references it, so the catalog
  6. //! cannot silently strip an entitlement's target.
  7. use crate::error::AppError;
  8. /// A catalog scope entry.
  9. #[derive(Debug, Clone, PartialEq, Eq)]
  10. pub struct ScopeRecord {
  11. /// Scope token (primary key).
  12. pub name: String,
  13. /// Human-readable description.
  14. pub description: String,
  15. /// Whether the scope is offered in discovery and may be requested.
  16. pub enabled: bool,
  17. }
  18. /// Insert a scope or update its description, preserving its `enabled` state.
  19. ///
  20. /// Idempotent, so `config sync` may run repeatedly: a scope disabled by an
  21. /// operator is not silently re-enabled by a later sync.
  22. ///
  23. /// # Errors
  24. /// Returns an error on any database failure.
  25. pub async fn upsert(
  26. pool: &sqlx::PgPool,
  27. name: &str,
  28. description: &str,
  29. ) -> Result<ScopeRecord, AppError> {
  30. let record = sqlx::query_as!(
  31. ScopeRecord,
  32. r#"INSERT INTO scopes (name, description, enabled, created_at, updated_at)
  33. VALUES ($1, $2, true, now(), now())
  34. ON CONFLICT (name) DO UPDATE
  35. SET description = EXCLUDED.description, updated_at = now()
  36. RETURNING name, description, enabled"#,
  37. name,
  38. description,
  39. )
  40. .fetch_one(pool)
  41. .await?;
  42. Ok(record)
  43. }
  44. /// Look up a scope by name; `None` when absent.
  45. ///
  46. /// # Errors
  47. /// Returns an error on any database failure.
  48. pub async fn get(pool: &sqlx::PgPool, name: &str) -> Result<Option<ScopeRecord>, AppError> {
  49. let record = sqlx::query_as!(
  50. ScopeRecord,
  51. r#"SELECT name, description, enabled FROM scopes WHERE name = $1"#,
  52. name,
  53. )
  54. .fetch_optional(pool)
  55. .await?;
  56. Ok(record)
  57. }
  58. /// List every enabled scope, ordered by name.
  59. ///
  60. /// # Errors
  61. /// Returns an error on any database failure.
  62. pub async fn list_enabled(pool: &sqlx::PgPool) -> Result<Vec<ScopeRecord>, AppError> {
  63. let records = sqlx::query_as!(
  64. ScopeRecord,
  65. r#"SELECT name, description, enabled FROM scopes
  66. WHERE enabled ORDER BY name"#,
  67. )
  68. .fetch_all(pool)
  69. .await?;
  70. Ok(records)
  71. }
  72. /// List every scope (enabled and disabled), ordered by name.
  73. ///
  74. /// # Errors
  75. /// Returns an error on any database failure.
  76. pub async fn list_all(pool: &sqlx::PgPool) -> Result<Vec<ScopeRecord>, AppError> {
  77. let records = sqlx::query_as!(
  78. ScopeRecord,
  79. r#"SELECT name, description, enabled FROM scopes ORDER BY name"#,
  80. )
  81. .fetch_all(pool)
  82. .await?;
  83. Ok(records)
  84. }
  85. /// Enable or disable a scope. Returns whether a row was updated.
  86. ///
  87. /// # Errors
  88. /// Returns an error on any database failure.
  89. pub async fn set_enabled(pool: &sqlx::PgPool, name: &str, enabled: bool) -> Result<bool, AppError> {
  90. let result = sqlx::query!(
  91. r#"UPDATE scopes SET enabled = $2, updated_at = now() WHERE name = $1"#,
  92. name,
  93. enabled,
  94. )
  95. .execute(pool)
  96. .await?;
  97. Ok(result.rows_affected() == 1)
  98. }
  99. /// Delete a scope only when no `user_scopes` entitlement references it.
  100. ///
  101. /// Returns `true` when a row was deleted. Deleting a referenced scope would
  102. /// either cascade away entitlements or violate the foreign key, so this is a
  103. /// guarded no-op instead.
  104. ///
  105. /// # Errors
  106. /// Returns an error on any database failure.
  107. pub async fn delete_if_unreferenced(pool: &sqlx::PgPool, name: &str) -> Result<bool, AppError> {
  108. let result = sqlx::query!(
  109. r#"DELETE FROM scopes
  110. WHERE name = $1
  111. AND NOT EXISTS (SELECT 1 FROM user_scopes WHERE scope_name = $1)"#,
  112. name,
  113. )
  114. .execute(pool)
  115. .await?;
  116. Ok(result.rows_affected() == 1)
  117. }