bios_basic/spi/
spi_initializer.rs

1//! SPI initializer
2use tardis::basic::dto::TardisContext;
3use tardis::basic::field::TrimString;
4use tardis::basic::result::TardisResult;
5use tardis::db::reldb_client::TardisActiveModel;
6use tardis::{TardisFuns, TardisFunsInst};
7
8use crate::rbum::dto::rbum_domain_dto::RbumDomainAddReq;
9use crate::rbum::dto::rbum_filer_dto::{RbumBasicFilterReq, RbumKindFilterReq};
10use crate::rbum::dto::rbum_kind_dto::RbumKindAddReq;
11use crate::rbum::rbum_enumeration::RbumScopeLevelKind;
12use crate::rbum::serv::rbum_crud_serv::RbumCrudOperation;
13use crate::rbum::serv::rbum_domain_serv::RbumDomainServ;
14use crate::rbum::serv::rbum_kind_serv::RbumKindServ;
15
16use super::domain::spi_bs;
17
18/// SPI service initialization
19/// SPI服务初始化
20///
21/// Initialize ``rbum_domain`` for different SPI services
22/// 初始化不同SPI服务的``rbum_domain``
23pub async fn init(code: &str, funs: &TardisFunsInst) -> TardisResult<TardisContext> {
24    let ctx = TardisContext {
25        own_paths: "".to_string(),
26        ak: "_".to_string(),
27        roles: vec![],
28        groups: vec![],
29        owner: "".to_string(),
30        ..Default::default()
31    };
32    if RbumDomainServ::get_rbum_domain_id_by_code(code, funs).await?.is_some() {
33        return Ok(ctx);
34    }
35    // Initialize spi component RBUM item table and indexes
36    funs.db().init(spi_bs::ActiveModel::init(TardisFuns::reldb().backend(), None, TardisFuns::reldb().compatible_type())).await?;
37    // Initialize spi component RBUM domain data
38    RbumDomainServ::add_rbum(
39        &mut RbumDomainAddReq {
40            code: TrimString(code.to_string()),
41            name: TrimString(code.to_string()),
42            note: None,
43            icon: None,
44            sort: None,
45            scope_level: Some(RbumScopeLevelKind::Root),
46        },
47        funs,
48        &ctx,
49    )
50    .await?;
51    Ok(ctx)
52}
53
54/// Add the type of the SPI service backend implementation
55/// 添加SPI服务后端实现的类型
56pub async fn add_kind(scheme: &str, funs: &TardisFunsInst, ctx: &TardisContext) -> TardisResult<()> {
57    if !RbumKindServ::exist_rbum(
58        &RbumKindFilterReq {
59            basic: RbumBasicFilterReq {
60                code: Some(scheme.to_string()),
61                ..Default::default()
62            },
63            ..Default::default()
64        },
65        funs,
66        ctx,
67    )
68    .await?
69    {
70        RbumKindServ::add_rbum(
71            &mut RbumKindAddReq {
72                code: TrimString(scheme.to_string()),
73                name: TrimString(scheme.to_string()),
74                note: None,
75                icon: None,
76                sort: None,
77                module: None,
78                ext_table_name: Some("spi_bs".to_lowercase()),
79                scope_level: Some(RbumScopeLevelKind::Root),
80            },
81            funs,
82            ctx,
83        )
84        .await?;
85    }
86    Ok(())
87}
88
89/// Some common initialization helper methods
90/// 一些公共的初始化辅助方法
91///
92/// # Mainly includes the following functions:
93/// 1. Isolation flag processing.
94///    The isolation identifier is the data used to distinguish the subject of the request (tenant or application).
95///    For example, application a and application b use the same cache service instance and both use the same cache key.
96///    To avoid data confusion, you can use the isolation flag as part of the cache key to distinguish different application data.
97///    For the private [`crate::spi::domain::spi_bs::Model::private`] backend implementation instance, the isolation flag is not required,
98///    because the private instance will only be used by one subject of the request.
99///
100/// # 主要包含如下功能:
101/// 1. 隔离标识处理。隔离标识是用于区分请求主体(租户或应用)的数据。
102///    比如应用a与应用b使用相同的缓存服务实例且都使用了相同的缓存key,为了避免数据混乱,可以通过隔离标识作为缓存key的一部分来区分不同的应用数据。
103///    对于私有 [`crate::spi::domain::spi_bs::Model::private`] 的后端实现实例不需要隔离标识,因为私有实例只会被一个请求主体使用。
104pub mod common {
105    use std::collections::HashMap;
106
107    use tardis::{basic::dto::TardisContext, TardisFuns};
108
109    use crate::spi::spi_constants;
110
111    /// Get the context's ``owner`` as the isolation flag
112    /// 获取上下文的``owner``作为隔离标识
113    pub fn get_isolation_flag_from_context(ctx: &TardisContext) -> String {
114        // Fix case insensitivity
115        format!("spi{}", TardisFuns::crypto.hex.encode(&ctx.ak))
116    }
117
118    /// Set the isolation flag to the extension
119    /// 将隔离标识设置到扩展中
120    pub fn set_isolation_flag_to_ext(isolation_flag: &str, ext: &mut HashMap<String, String>) {
121        ext.insert(spi_constants::SPI_ISOLATION_FLAG.to_string(), isolation_flag.to_string());
122    }
123
124    /// Get the isolation flag from the extension
125    /// 从扩展中获取隔离标识
126    pub fn get_isolation_flag_from_ext(ext: &HashMap<String, String>) -> Option<String> {
127        ext.get(spi_constants::SPI_ISOLATION_FLAG).map(|s| s.to_string())
128    }
129}
130
131/// Some common postgresql initialization helper methods
132/// 一些公共的PostgreSQL初始化辅助方法
133pub mod common_pg {
134    use std::collections::HashMap;
135
136    use tardis::{
137        basic::{dto::TardisContext, error::TardisError, result::TardisResult},
138        config::config_dto::DBModuleConfig,
139        db::{
140            reldb_client::{TardisRelDBClient, TardisRelDBlConnection},
141            sea_orm::Value,
142        },
143        TardisFuns,
144    };
145
146    use crate::spi::{
147        dto::spi_bs_dto::SpiBsCertResp,
148        spi_constants::GLOBAL_STORAGE_FLAG,
149        spi_funs::{SpiBsInst, TypedSpiBsInst},
150    };
151
152    use super::common;
153
154    /// Get the schema name from the context
155    /// 从上下文中获取schema名称
156    ///
157    /// Each subject of the request (tenant or application) data will be isolated into different schemas, and the schema name is determined by the isolation flag
158    /// 每个请求主体(租户或应用)的数据都会被隔离到不同的schema中,schema的名称由隔离标识决定
159    pub fn get_schema_name_from_context(ctx: &TardisContext) -> String {
160        common::get_isolation_flag_from_context(ctx)
161    }
162
163    /// Set the schema name to the extension
164    /// 将schema名称设置到扩展中
165    ///
166    /// Each subject of the request (tenant or application) data will be isolated into different schemas, and the schema name is determined by the isolation flag
167    /// 每个请求主体(租户或应用)的数据都会被隔离到不同的schema中,schema的名称由隔离标识决定
168    pub fn set_schema_name_to_ext(schema_name: &str, ext: &mut HashMap<String, String>) {
169        common::set_isolation_flag_to_ext(schema_name, ext);
170    }
171
172    /// Get the schema name from the extension
173    /// 从扩展中获取schema名称
174    ///
175    /// Each subject of the request (tenant or application) data will be isolated into different schemas, and the schema name is determined by the isolation flag
176    /// 每个请求主体(租户或应用)的数据都会被隔离到不同的schema中,schema的名称由隔离标识决定
177    pub fn get_schema_name_from_ext(ext: &HashMap<String, String>) -> Option<String> {
178        common::get_isolation_flag_from_ext(ext)
179    }
180
181    /// Get the table full name from the extension
182    /// 根据入参生成对应表全限定名
183    pub fn get_table_full_name(ext: &HashMap<String, String>, table_flag: String, tag: String) -> String {
184        let schema_name = get_schema_name_from_ext(ext).expect("ignore");
185        format!("{schema_name}.{GLOBAL_STORAGE_FLAG}_{table_flag}_{tag}")
186    }
187
188    /// Check if the schema exists
189    /// 检查schema是否存在
190    pub async fn check_schema_exit(client: &TardisRelDBClient, ctx: &TardisContext) -> TardisResult<bool> {
191        let schema_name = get_schema_name_from_context(ctx);
192        let schema = client.conn().count_by_sql("SELECT 1 FROM information_schema.schemata WHERE schema_name = $1", vec![Value::from(schema_name.as_str())]).await?;
193        Ok(schema != 0)
194    }
195
196    /// Create schema
197    /// 创建schema
198    pub async fn create_schema(client: &TardisRelDBClient, ctx: &TardisContext) -> TardisResult<String> {
199        let schema_name = get_schema_name_from_context(ctx);
200        if !check_schema_exit(client, ctx).await? {
201            client.conn().execute_one(&format!("CREATE SCHEMA {schema_name}"), vec![]).await?;
202        }
203        Ok(schema_name)
204    }
205
206    /// Check if the table exists
207    /// 检查表是否存在
208    ///
209    /// When checking, the schema corresponding to the request will be added
210    /// 检查时会加上与请求对应的schema
211    pub async fn check_table_exit(table_name: &str, conn: &TardisRelDBlConnection, ctx: &TardisContext) -> TardisResult<bool> {
212        let schema_name = get_schema_name_from_context(ctx);
213        let table = conn
214            .count_by_sql(
215                "SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2",
216                vec![Value::from(schema_name.as_str()), Value::from(format!("{GLOBAL_STORAGE_FLAG}_{table_name}"))],
217            )
218            .await?;
219        Ok(table != 0)
220    }
221
222    /// Set the schema to the session
223    /// 将schema设置到会话中
224    ///
225    /// After setting, the operation of the current connection does not need to add the schema prefix
226    /// 设置后当前连接的操作不需要再加上schema前缀
227    pub async fn set_schema_to_session(schema_name: &str, conn: &mut TardisRelDBlConnection) -> TardisResult<()> {
228        conn.begin().await?;
229        conn.execute_one(&format!("SET SCHEMA '{schema_name}'"), vec![]).await?;
230        Ok(())
231    }
232
233    /// Get the formatted table name
234    /// 获取格式化后的表名
235    ///
236    /// Format: {schema_name}.{GLOBAL_STORAGE_FLAG}_{table_name}
237    /// 格式为:{schema_name}.{GLOBAL_STORAGE_FLAG}_{table_name}
238    pub fn package_table_name(table_name: &str, ctx: &TardisContext) -> String {
239        let schema_name = get_schema_name_from_context(ctx);
240        format!("{schema_name}.{GLOBAL_STORAGE_FLAG}_{table_name}")
241    }
242
243    /// Initialize the backend implementation instance of PostgreSQL
244    /// 初始化PostgreSQL的后端实现实例
245    pub async fn init(bs_cert: &SpiBsCertResp, ctx: &TardisContext, mgr: bool) -> TardisResult<SpiBsInst> {
246        let ext = TardisFuns::json.str_to_json(&bs_cert.ext)?;
247        let compatible_type = TardisFuns::json.json_to_obj(ext.get("compatible_type").unwrap_or(&tardis::serde_json::Value::String("None".to_string())).clone())?;
248        let client = TardisRelDBClient::init(&DBModuleConfig {
249            url: bs_cert.conn_uri.parse().expect("invalid url"),
250            max_connections: ext.get("max_connections").and_then(|c| c.as_u64()).unwrap_or(5) as u32,
251            min_connections: ext.get("min_connections").and_then(|c| c.as_u64()).unwrap_or(1) as u32,
252            connect_timeout_sec: None,
253            idle_timeout_sec: None,
254            compatible_type,
255        })
256        .await?;
257        let mut ext = HashMap::new();
258        // If the connection is private, the isolation (schema) does not need to be processed, so use public directly.
259        // 如果连接为私有的,不需要处理隔离(schema),故直接使用public
260        let schema_name = if bs_cert.private {
261            "public".to_string()
262        } else if mgr {
263            // Only in management mode can the schema be created
264            // 仅管理模式下才能创建schema
265            create_schema(&client, ctx).await?
266        } else if check_schema_exit(&client, ctx).await? {
267            get_schema_name_from_context(ctx)
268        } else {
269            return Err(TardisError::bad_request("The requested schema does not exist", ""));
270        };
271        set_schema_name_to_ext(&schema_name, &mut ext);
272        Ok(SpiBsInst { client: Box::new(client), ext })
273    }
274
275    /// Initialize the table and connection
276    /// 初始化表和连接
277    pub async fn init_table_and_conn(
278        // Database connection client instance
279        bs_inst: TypedSpiBsInst<'_, TardisRelDBClient>,
280        ctx: &TardisContext,
281        // If it is management mode
282        mgr: bool,
283        // Tag, as a table name suffix
284        tag: Option<&str>,
285        // Table flag, as part of the table name
286        table_flag: &str,
287        // Create table DDL
288        table_create_content: &str,
289        table_inherits: Option<String>,
290        // Table index
291        // Format: field name -> index type
292        indexes: Vec<(&str, &str)>,
293        // Primary keys
294        primary_keys: Option<Vec<&str>>,
295        // Update time field, used to create a trigger that automatically updates the time
296        update_time_field: Option<&str>,
297    ) -> TardisResult<(TardisRelDBlConnection, String)> {
298        let tag = tag.map(|t| format!("_{t}")).unwrap_or_default();
299        let conn = bs_inst.0.conn();
300        let schema_name = get_schema_name_from_ext(bs_inst.1).expect("ignore");
301        if check_table_exit(&format!("{table_flag}{tag}"), &conn, ctx).await? {
302            return Ok((conn, format!("{schema_name}.{GLOBAL_STORAGE_FLAG}_{table_flag}{tag}")));
303        } else if !mgr {
304            return Err(TardisError::bad_request("The requested tag does not exist", ""));
305        }
306        do_init_table(
307            &schema_name,
308            &conn,
309            &tag,
310            table_flag,
311            table_create_content,
312            table_inherits,
313            indexes,
314            primary_keys,
315            update_time_field,
316        )
317        .await?;
318        Ok((conn, format!("{schema_name}.{GLOBAL_STORAGE_FLAG}_{table_flag}{tag}")))
319    }
320
321    /// Initialize connection
322    /// 初始化连接
323    pub async fn init_conn(bs_inst: TypedSpiBsInst<'_, TardisRelDBClient>) -> TardisResult<(TardisRelDBlConnection, String)> {
324        let conn = bs_inst.0.conn();
325        let schema_name = get_schema_name_from_ext(bs_inst.1).expect("ignore");
326        Ok((conn, schema_name))
327    }
328
329    /// Initialize table
330    /// 初始化表
331    pub async fn init_table(
332        conn: &TardisRelDBlConnection,
333        tag: Option<&str>,
334        table_flag: &str,
335        table_create_content: &str,
336        // field_name_or_fun -> index type
337        indexes: Vec<(&str, &str)>,
338        primary_keys: Option<Vec<&str>>,
339        update_time_field: Option<&str>,
340        ctx: &TardisContext,
341    ) -> TardisResult<()> {
342        let tag = tag.map(|t| format!("_{t}")).unwrap_or_default();
343        let schema_name = get_schema_name_from_context(ctx);
344        do_init_table(&schema_name, conn, &tag, table_flag, table_create_content, None, indexes, primary_keys, update_time_field).await
345    }
346
347    async fn do_init_table(
348        schema_name: &str,
349        conn: &TardisRelDBlConnection,
350        tag: &str,
351        table_flag: &str,
352        table_create_content: &str,
353        table_inherits: Option<String>,
354        // field_name_or_fun -> index type
355        indexes: Vec<(&str, &str)>,
356        primary_keys: Option<Vec<&str>>,
357        update_time_field: Option<&str>,
358    ) -> TardisResult<()> {
359        conn.execute_one(
360            &format!(
361                r#"CREATE TABLE {schema_name}.{GLOBAL_STORAGE_FLAG}_{table_flag}{tag}
362(
363    {table_create_content}
364            ){}"#,
365                if let Some(inherits) = table_inherits {
366                    format!(" INHERITS ({inherits})")
367                } else {
368                    "".to_string()
369                }
370            ),
371            vec![],
372        )
373        .await?;
374        for (idx, (field_name_or_fun, index_type)) in indexes.into_iter().enumerate() {
375            // index name shouldn't be longer than 63 characters
376            // [4 ][     18    ][ 12 ][     26    ][ 3 ]
377            // idx_{schema_name}{tag}_{table_flag}_{idx}
378            #[inline]
379            fn truncate_str(s: &str, max_size: usize) -> &str {
380                &s[..max_size.min(s.len())]
381            }
382            let index_name = format!(
383                "idx_{schema_name}{tag}_{table_flag}_{idx}",
384                schema_name = truncate_str(schema_name, 18),
385                tag = truncate_str(tag, 11),
386                table_flag = truncate_str(table_flag, 25),
387                idx = truncate_str(idx.to_string().as_str(), 3),
388            );
389            conn.execute_one(
390                &format!("CREATE INDEX {index_name} ON {schema_name}.{GLOBAL_STORAGE_FLAG}_{table_flag}{tag} USING {index_type}({field_name_or_fun})"),
391                vec![],
392            )
393            .await?;
394        }
395        if let Some(primary_keys) = primary_keys {
396            let pks = primary_keys.join(", ");
397            conn.execute_one(
398                &format!(r#"ALTER TABLE {schema_name}.{GLOBAL_STORAGE_FLAG}_{table_flag}{tag} ADD PRIMARY KEY ({pks})"#),
399                vec![],
400            )
401            .await?;
402        }
403        if let Some(update_time_field) = update_time_field {
404            conn.execute_one(
405                &format!(
406                    r###"CREATE OR REPLACE FUNCTION TARDIS_AUTO_UPDATE_TIME_{}()
407RETURNS TRIGGER AS $$
408BEGIN
409    NEW.{} = now();
410    RETURN NEW;
411END;
412$$ language 'plpgsql';"###,
413                    update_time_field.replace('-', "_"),
414                    update_time_field
415                ),
416                vec![],
417            )
418            .await?;
419            conn.execute_one(
420                &format!(
421                    r###"CREATE OR REPLACE TRIGGER TARDIS_AUTO_UPDATE_TIME_ON
422    BEFORE UPDATE
423    ON
424        {}.{GLOBAL_STORAGE_FLAG}_{}{}
425    FOR EACH ROW
426EXECUTE PROCEDURE TARDIS_AUTO_UPDATE_TIME_{}();"###,
427                    schema_name,
428                    table_flag,
429                    tag,
430                    update_time_field.replace('-', "_")
431                ),
432                vec![],
433            )
434            .await?;
435        }
436        Ok(())
437    }
438}