1use utoipa::ToSchema;
6
7use crate::prelude::*;
8
9pub const RETENTION_DAYS: i64 = 90;
11
12pub const FULL_BODY_ITEM_LIMIT: usize = 20;
14
15pub const SAMPLED_BODY_ITEM_COUNT: usize = 5;
17
18pub const BODY_SAMPLE_MAX_BYTES: usize = 64 * 1024;
20
21#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, Type, ToSchema)]
22#[sqlx(type_name = "suotar_endpoint", rename_all = "snake_case")]
23#[serde(rename_all = "snake_case")]
24pub enum SuotarEndpoint {
25 ResolvePersons,
26 ResolveEnrolments,
27 ImportAttainments,
28 VerifyAttainments,
29 ProductAccessTokens,
30 ListByCourse,
31}
32
33#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
34pub struct SuotarApiCall {
35 pub id: Uuid,
36 pub created_at: DateTime<Utc>,
37 pub updated_at: DateTime<Utc>,
38 pub deleted_at: Option<DateTime<Utc>>,
39 pub endpoint: SuotarEndpoint,
40 pub request_item_count: i32,
41 pub http_status: Option<i32>,
42 pub duration_ms: Option<i32>,
43 pub succeeded: bool,
44 pub ok_item_count: i32,
45 pub error_item_count: i32,
46 pub request_level_error_code: Option<String>,
47 pub error_message: Option<String>,
48 pub request_body_sample: Option<serde_json::Value>,
49 pub response_body_sample: Option<serde_json::Value>,
50 pub credit_registration_ids: Vec<Uuid>,
51 pub worker_name: String,
52 pub started_at: DateTime<Utc>,
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct NewSuotarApiCall {
57 pub endpoint: SuotarEndpoint,
58 pub request_item_count: i32,
59 pub http_status: Option<i32>,
60 pub duration_ms: Option<i32>,
61 pub succeeded: bool,
62 pub ok_item_count: i32,
63 pub error_item_count: i32,
64 pub request_level_error_code: Option<String>,
65 pub error_message: Option<String>,
67 pub request_body_sample: Option<serde_json::Value>,
69 pub response_body_sample: Option<serde_json::Value>,
71 pub credit_registration_ids: Vec<Uuid>,
72 pub worker_name: String,
73 pub started_at: DateTime<Utc>,
74}
75
76pub async fn insert(conn: &mut PgConnection, new: &NewSuotarApiCall) -> ModelResult<Uuid> {
77 let res = sqlx::query!(
78 r#"
79INSERT INTO suotar_api_calls (
80 endpoint,
81 request_item_count,
82 http_status,
83 duration_ms,
84 succeeded,
85 ok_item_count,
86 error_item_count,
87 request_level_error_code,
88 error_message,
89 request_body_sample,
90 response_body_sample,
91 credit_registration_ids,
92 worker_name,
93 started_at
94 )
95VALUES (
96 $1,
97 $2,
98 $3,
99 $4,
100 $5,
101 $6,
102 $7,
103 $8,
104 $9,
105 $10,
106 $11,
107 $12,
108 $13,
109 $14
110 )
111RETURNING id
112 "#,
113 new.endpoint as SuotarEndpoint,
114 new.request_item_count,
115 new.http_status,
116 new.duration_ms,
117 new.succeeded,
118 new.ok_item_count,
119 new.error_item_count,
120 new.request_level_error_code,
121 new.error_message,
122 new.request_body_sample,
123 new.response_body_sample,
124 &new.credit_registration_ids,
125 new.worker_name,
126 new.started_at,
127 )
128 .fetch_one(conn)
129 .await?;
130 Ok(res.id)
131}
132
133pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<SuotarApiCall> {
134 let res = sqlx::query_as!(
135 SuotarApiCall,
136 r#"
137SELECT *
138FROM suotar_api_calls
139WHERE id = $1
140 AND deleted_at IS NULL
141 "#,
142 id
143 )
144 .fetch_one(conn)
145 .await?;
146 Ok(res)
147}
148
149pub async fn get_recent(conn: &mut PgConnection, limit: i64) -> ModelResult<Vec<SuotarApiCall>> {
150 let res = sqlx::query_as!(
151 SuotarApiCall,
152 r#"
153SELECT *
154FROM suotar_api_calls
155WHERE deleted_at IS NULL
156ORDER BY started_at DESC
157LIMIT $1
158 "#,
159 limit
160 )
161 .fetch_all(conn)
162 .await?;
163 Ok(res)
164}
165
166pub async fn get_recent_by_endpoint(
167 conn: &mut PgConnection,
168 endpoint: SuotarEndpoint,
169 limit: i64,
170) -> ModelResult<Vec<SuotarApiCall>> {
171 let res = sqlx::query_as!(
172 SuotarApiCall,
173 r#"
174SELECT *
175FROM suotar_api_calls
176WHERE endpoint = $1
177 AND deleted_at IS NULL
178ORDER BY started_at DESC
179LIMIT $2
180 "#,
181 endpoint as SuotarEndpoint,
182 limit,
183 )
184 .fetch_all(conn)
185 .await?;
186 Ok(res)
187}
188
189pub async fn get_recent_failures(
190 conn: &mut PgConnection,
191 limit: i64,
192) -> ModelResult<Vec<SuotarApiCall>> {
193 let res = sqlx::query_as!(
194 SuotarApiCall,
195 r#"
196SELECT *
197FROM suotar_api_calls
198WHERE NOT succeeded
199 AND deleted_at IS NULL
200ORDER BY started_at DESC
201LIMIT $1
202 "#,
203 limit
204 )
205 .fetch_all(conn)
206 .await?;
207 Ok(res)
208}
209
210pub async fn get_by_credit_registration_id(
214 conn: &mut PgConnection,
215 credit_registration_id: Uuid,
216 limit: i64,
217) -> ModelResult<Vec<SuotarApiCall>> {
218 let res = sqlx::query_as!(
219 SuotarApiCall,
220 r#"
221SELECT *
222FROM suotar_api_calls
223WHERE credit_registration_ids @> ARRAY [$1::uuid]
224 AND deleted_at IS NULL
225ORDER BY started_at DESC
226LIMIT $2
227 "#,
228 credit_registration_id,
229 limit,
230 )
231 .fetch_all(conn)
232 .await?;
233 Ok(res)
234}
235
236pub async fn delete_older_than(conn: &mut PgConnection, cutoff: DateTime<Utc>) -> ModelResult<u64> {
238 let res = sqlx::query!(
239 r#"
240DELETE FROM suotar_api_calls
241WHERE started_at < $1
242 "#,
243 cutoff
244 )
245 .execute(conn)
246 .await?;
247 Ok(res.rows_affected())
248}