Skip to main content

tibba_model/
configuration.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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(&params.effective_start_time)?)
196        .bind(parse_primitive_datetime(&params.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}