1use super::Model;
16use super::user::{ROLE_ADMIN, ROLE_SUPER_ADMIN};
17use super::{
18 Error, JsonSnafu, ModelListParams, Schema, SchemaAllowCreate, SchemaAllowEdit, SchemaType,
19 SchemaView, SqlxSnafu, Status, format_datetime, new_schema_options, parse_primitive_datetime,
20};
21use http::header::{HeaderMap, HeaderName, HeaderValue};
22use serde::de::DeserializeOwned;
23use serde::{Deserialize, Serialize};
24use snafu::ResultExt;
25use sqlx::FromRow;
26use sqlx::types::Json;
27use sqlx::{Pool, Postgres, QueryBuilder};
28use std::collections::HashMap;
29use std::str::FromStr;
30use time::{OffsetDateTime, PrimitiveDateTime};
31
32type Result<T> = std::result::Result<T, Error>;
33
34#[derive(FromRow)]
35struct ConfigurationSchema {
36 id: i64,
37 status: i16,
38 category: String,
39 name: String,
40 data: Json<serde_json::Value>,
41 description: String,
42 effective_start_time: PrimitiveDateTime,
43 effective_end_time: PrimitiveDateTime,
44 created: PrimitiveDateTime,
45 modified: PrimitiveDateTime,
46}
47
48#[derive(Deserialize, Serialize)]
49pub struct Configuration {
50 pub id: i64,
51 pub status: i16,
52 pub category: String,
53 pub name: String,
54 pub data: HashMap<String, serde_json::Value>,
55 pub description: String,
56 pub effective_start_time: String,
57 pub effective_end_time: String,
58 pub created: String,
59 pub modified: String,
60}
61
62impl From<ConfigurationSchema> for Configuration {
63 fn from(schema: ConfigurationSchema) -> Self {
64 Self {
65 id: schema.id,
66 status: schema.status,
67 category: schema.category,
68 name: schema.name,
69 data: serde_json::from_value(schema.data.0).unwrap_or_default(),
70 description: schema.description,
71 effective_start_time: format_datetime(schema.effective_start_time),
72 effective_end_time: format_datetime(schema.effective_end_time),
73 created: format_datetime(schema.created),
74 modified: format_datetime(schema.modified),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Deserialize, Default)]
80pub struct ConfigurationInsertParams {
81 pub category: String,
82 pub name: String,
83 pub data: serde_json::Value,
84 pub description: Option<String>,
85 pub status: i16,
86 pub effective_start_time: String,
87 pub effective_end_time: String,
88}
89
90#[derive(Debug, Clone, Deserialize, Default)]
91pub struct ConfigurationUpdateParams {
92 pub data: Option<serde_json::Value>,
93 pub description: Option<String>,
94 pub status: Option<i16>,
95 pub effective_start_time: Option<String>,
96 pub effective_end_time: Option<String>,
97}
98
99#[derive(Debug, Clone, Deserialize, Default)]
100pub struct AlarmConfig {
101 pub category: String,
102 pub url: String,
103}
104
105#[derive(Default)]
106pub struct ConfigurationModel {}
107
108impl Model for ConfigurationModel {
109 type Output = Configuration;
110 fn new() -> Self {
111 Self::default()
112 }
113 async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
114 SchemaView {
115 schemas: vec![
116 Schema::new_id(),
117 Schema {
118 name: "name".to_string(),
119 category: SchemaType::String,
120 required: true,
121 read_only: true,
122 filterable: true,
123 fixed: true,
124 ..Default::default()
125 },
126 Schema {
127 name: "category".to_string(),
128 category: SchemaType::String,
129 required: true,
130 read_only: true,
131 filterable: true,
132 options: Some(new_schema_options(&[
133 "common",
134 "app",
135 "user",
136 "system",
137 "alarm",
138 "response_headers",
139 ])),
140 ..Default::default()
141 },
142 Schema::new_effective_start_time(),
143 Schema::new_effective_end_time(),
144 Schema {
145 name: "data".to_string(),
146 category: SchemaType::Json,
147 span: Some(2),
148 required: true,
149 popover: true,
150 ..Default::default()
151 },
152 Schema::new_status(),
153 Schema {
154 name: "description".to_string(),
155 category: SchemaType::String,
156 ..Default::default()
157 },
158 Schema::new_created(),
159 Schema::new_modified(),
160 ],
161 allow_edit: SchemaAllowEdit {
162 owner: true,
163 roles: vec![ROLE_SUPER_ADMIN.to_string(), ROLE_ADMIN.to_string()],
164 ..Default::default()
165 },
166 allow_create: SchemaAllowCreate {
167 roles: vec![ROLE_SUPER_ADMIN.to_string(), ROLE_ADMIN.to_string()],
168 ..Default::default()
169 },
170 }
171 }
172
173 fn push_filter_conditions<'args>(
174 &self,
175 qb: &mut QueryBuilder<'args, Postgres>,
176 filters: &HashMap<String, String>,
177 ) -> Result<()> {
178 if let Some(category) = filters.get("category") {
179 qb.push(" AND category = ");
180 qb.push_bind(category.clone());
181 }
182 Ok(())
183 }
184 async fn insert(&self, pool: &Pool<Postgres>, data: serde_json::Value) -> Result<u64> {
185 let params: ConfigurationInsertParams = serde_json::from_value(data).context(JsonSnafu)?;
186 let row: (i64,) = sqlx::query_as(
187 r#"
188 INSERT INTO configurations (category, name, data, description, status, effective_start_time, effective_end_time) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id"#,
189 )
190 .bind(params.category)
191 .bind(params.name)
192 .bind(params.data)
193 .bind(params.description)
194 .bind(params.status)
195 .bind(parse_primitive_datetime(¶ms.effective_start_time)?)
196 .bind(parse_primitive_datetime(¶ms.effective_end_time)?)
197 .fetch_one(pool)
198 .await
199 .context(SqlxSnafu)?;
200
201 Ok(row.0 as u64)
202 }
203
204 async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
205 let result = sqlx::query_as::<_, ConfigurationSchema>(
206 r#"SELECT * FROM configurations WHERE id = $1 AND deleted_at IS NULL"#,
207 )
208 .bind(id as i64)
209 .fetch_optional(pool)
210 .await
211 .context(SqlxSnafu)?;
212
213 Ok(result.map(|schema| schema.into()))
214 }
215
216 async fn delete_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<()> {
217 sqlx::query(
218 r#"UPDATE configurations SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL"#,
219 )
220 .bind(id as i64)
221 .execute(pool)
222 .await
223 .context(SqlxSnafu)?;
224
225 Ok(())
226 }
227
228 async fn update_by_id(
229 &self,
230 pool: &Pool<Postgres>,
231 id: u64,
232 data: serde_json::Value,
233 ) -> Result<()> {
234 let params: ConfigurationUpdateParams = serde_json::from_value(data).context(JsonSnafu)?;
235 let _ = sqlx::query(
236 r#"UPDATE configurations SET data = COALESCE($1, data), description = COALESCE($2, description), status = COALESCE($3, status), effective_start_time = COALESCE($4, effective_start_time), effective_end_time = COALESCE($5, effective_end_time) WHERE id = $6 AND deleted_at IS NULL"#,
237 )
238 .bind(params.data)
239 .bind(params.description)
240 .bind(params.status)
241 .bind(params.effective_start_time.as_deref().map(parse_primitive_datetime).transpose()?)
242 .bind(params.effective_end_time.as_deref().map(parse_primitive_datetime).transpose()?)
243 .bind(id as i64)
244 .execute(pool)
245 .await
246 .context(SqlxSnafu)?;
247
248 Ok(())
249 }
250
251 async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
252 let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM configurations");
253 self.push_conditions(&mut qb, params)?;
254 let count = qb
255 .build_query_scalar::<i64>()
256 .fetch_one(pool)
257 .await
258 .context(SqlxSnafu)?;
259 Ok(count)
260 }
261
262 async fn list(
263 &self,
264 pool: &Pool<Postgres>,
265 params: &ModelListParams,
266 ) -> Result<Vec<Self::Output>> {
267 let mut qb = QueryBuilder::new("SELECT * FROM configurations");
268 self.push_conditions(&mut qb, params)?;
269 params.push_pagination(&mut qb);
270 let configurations = qb
271 .build_query_as::<ConfigurationSchema>()
272 .fetch_all(pool)
273 .await
274 .context(SqlxSnafu)?;
275 Ok(configurations.into_iter().map(|s| s.into()).collect())
276 }
277}
278
279impl ConfigurationModel {
280 pub async fn get_response_headers(
281 &self,
282 pool: &Pool<Postgres>,
283 name: &str,
284 ) -> Result<Option<HeaderMap>> {
285 let now_utc = OffsetDateTime::now_utc();
286 let now = PrimitiveDateTime::new(now_utc.date(), now_utc.time());
287 let configurations = sqlx::query_as::<_, ConfigurationSchema>(
288 r#"SELECT * FROM configurations
289 WHERE category = 'response_headers'
290 AND status = $1
291 AND name = $2
292 AND deleted_at IS NULL
293 AND effective_start_time <= $3
294 AND effective_end_time >= $4"#,
295 )
296 .bind(Status::Enabled as i16)
297 .bind(name)
298 .bind(now)
299 .bind(now)
300 .fetch_all(pool)
301 .await
302 .context(SqlxSnafu)?;
303
304 let mut headers = HeaderMap::new();
305
306 for configuration in configurations {
307 let data = configuration.data;
308 let Some(data) = data.as_object() else {
309 continue;
310 };
311 for (key, value) in data.iter() {
312 let Some(value_str) = value.as_str() else {
313 continue;
314 };
315 let Ok(header_value) = HeaderValue::from_str(value_str) else {
316 continue;
317 };
318 let Ok(header_name) = HeaderName::from_str(key) else {
319 continue;
320 };
321 headers.insert(header_name, header_value);
322 }
323 }
324 Ok(Some(headers))
325 }
326 pub async fn get_config<T>(
327 &self,
328 pool: &Pool<Postgres>,
329 category: &str,
330 name: &str,
331 ) -> Result<Option<T>>
332 where
333 T: DeserializeOwned,
334 {
335 let now_utc = OffsetDateTime::now_utc();
336 let now = PrimitiveDateTime::new(now_utc.date(), now_utc.time());
337 let configuration = sqlx::query_as::<_, ConfigurationSchema>(
338 r#"SELECT * FROM configurations
339 WHERE category = $1
340 AND status = $2
341 AND name = $3
342 AND deleted_at IS NULL
343 AND effective_start_time <= $4
344 AND effective_end_time >= $5"#,
345 )
346 .bind(category)
347 .bind(Status::Enabled as i16)
348 .bind(name)
349 .bind(now)
350 .bind(now)
351 .fetch_optional(pool)
352 .await
353 .context(SqlxSnafu)?;
354
355 let Some(configuration) = configuration else {
356 return Ok(None);
357 };
358 let data = configuration.data;
359 let Some(data) = data.as_object() else {
360 return Err(Error::NotFound);
361 };
362 let data: T =
363 serde_json::from_value(serde_json::Value::Object(data.clone())).context(JsonSnafu)?;
364 Ok(Some(data))
365 }
366}