couchdb_orm/client/couchdb/actions/db/migrate/
mod.rs

1// Copyright (C) 2020-2023  OpenToolAdd
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15// contact: contact@tool-add.com
16
17use crate::client::couchdb::actions::db::all_docs::get_all_docs;
18use crate::regexes::COUCHDB_DB_RULE;
19use awc::Client;
20use serde::{de::DeserializeOwned, Serialize};
21use std::fmt::Debug;
22
23pub mod errors;
24
25use errors::DBMigrationError;
26
27async fn post_request<T: Serialize + DeserializeOwned + Debug>(
28    client: &awc::Client,
29    uri: &str,
30    body: &Vec<T>,
31) -> Result<bool, Box<dyn std::error::Error>> {
32    let json_body: serde_json::Value = serde_json::json!({ "docs": body });
33    match client.post(uri).send_json(&json_body).await {
34        Ok(mut response) => {
35            // println!("{:?}", response);
36            if response.status().as_u16() >= 400 {
37                let bytes: Vec<u8> = response.body().await?.iter().cloned().collect();
38                let error: serde_json::Value =
39                    serde_json::from_str(std::str::from_utf8(bytes.as_slice()).unwrap()).unwrap();
40                Err(Box::new(DBMigrationError::new(&format!(
41                    "Error with the request to {}: error: \n{}",
42                    &uri, error
43                ))))
44            } else {
45                Ok(true)
46            }
47        }
48        Err(error) => Err(Box::new(DBMigrationError::new(&format!(
49            "Error with the request to {}: error: \n{}",
50            uri, error
51        )))),
52    }
53}
54
55pub async fn migrate_db<
56    D: From<S> + Serialize + DeserializeOwned + Debug,
57    S: Clone + Serialize + DeserializeOwned + Debug,
58>(
59    client: &Client,
60    db_name: &str,
61    host: &str,
62) -> Result<bool, Box<dyn std::error::Error>> {
63    if !COUCHDB_DB_RULE.is_match(db_name) {
64        return Err(Box::new(DBMigrationError::new(&format!(
65            "{} string doesn't respect the regex rule {}",
66            db_name,
67            COUCHDB_DB_RULE.as_str()
68        ))));
69    }
70
71    let mut skip: usize = 0;
72    let mut max_rows: usize = 1000;
73    let limit: usize = 100;
74    let uri: String = format!("{}/{}/_bulk_docs", host, db_name,);
75    // println!("{}", uri);
76
77    while skip < max_rows {
78        println!("chunk {} migrated", skip / 100);
79        // println!("max rows {}", max_rows);
80        // println!("skip {}", skip);
81        // println!("limit {}", limit);
82        let all_docs = get_all_docs::<S>(client, db_name, host, skip, limit).await?;
83        max_rows = all_docs.total_rows;
84        let new_docs: Vec<D> = all_docs
85            .rows
86            .iter()
87            .map(|old| {
88                let value: S = old.doc.clone();
89                D::from(value)
90            })
91            .collect();
92        post_request(&client, &uri, &new_docs).await?;
93        // println!("{:?}", resp);
94
95        skip += limit;
96    }
97    Ok(true)
98}
99
100#[cfg(test)]
101mod tests {
102    extern crate actix_web;
103    extern crate couchdb_orm_meta;
104    extern crate wiremock;
105
106    use super::*;
107    use std::error::Error;
108    use std::fs;
109    use std::path::PathBuf;
110    use std::str::FromStr;
111
112    use wiremock::matchers::{method, path, path_regex};
113    use wiremock::{Mock, MockServer, ResponseTemplate};
114
115    use couchdb_orm_meta::schemas::tasks::schema_1661680133::Task as OldTask;
116    use couchdb_orm_meta::schemas::tasks::schema_1661681370::Task;
117
118    #[actix_rt::test]
119    async fn migrate_db_regex_error_test() {
120        let client: Client = Client::default();
121        let box_result: Box<dyn Error> = migrate_db::<Task, OldTask>(&client, "Test", "")
122            .await
123            .unwrap_err();
124        let result: &DBMigrationError = box_result.downcast_ref().unwrap();
125        assert_eq!(
126            format!("{}", result),
127            format!(
128                "DBMigrationError: {} string doesn't respect the regex rule {}",
129                "Test",
130                COUCHDB_DB_RULE.as_str()
131            )
132        );
133
134        let box_result: Box<dyn Error> = migrate_db::<Task, OldTask>(&client, "testT", "")
135            .await
136            .unwrap_err();
137        let result: &DBMigrationError = box_result.downcast_ref().unwrap();
138        assert_eq!(
139            format!("{}", result),
140            format!(
141                "DBMigrationError: {} string doesn't respect the regex rule {}",
142                "testT",
143                COUCHDB_DB_RULE.as_str()
144            )
145        )
146    }
147
148    #[actix_rt::test]
149    async fn migrate_db_http_error_test() {
150        let mock_server = MockServer::start().await;
151        // Some JSON input data as a &str. Maybe this comes from the user.
152        let data = r#"
153        {
154            "error": "test error"
155        }"#;
156        // Open the file in read-only mode with buffer.
157        let this_file: PathBuf = PathBuf::from_str(file!()).unwrap();
158        let file = fs::read_to_string(&format!(
159            "{}/__fixtures__/_all_docs.200.response.json",
160            this_file.parent().unwrap().to_str().unwrap()
161        ))
162        .unwrap();
163
164        let all_docs_response: ResponseTemplate =
165            ResponseTemplate::new(200).set_body_raw(file.as_bytes(), "application/json");
166        let bulk_docs_response: ResponseTemplate =
167            ResponseTemplate::new(400).set_body_raw(data.as_bytes(), "application/json");
168        let all_docs_mock = Mock::given(method("POST"))
169            .and(path_regex(r"/test/\w+"))
170            .respond_with(all_docs_response);
171        let bulk_docs_mock = Mock::given(method("POST"))
172            .and(path("/test/_bulk_docs"))
173            .respond_with(bulk_docs_response);
174
175        mock_server.register(bulk_docs_mock).await;
176        mock_server.register(all_docs_mock).await;
177
178        let error: serde_json::Value = serde_json::from_str(data).unwrap();
179
180        let client: Client = Client::default();
181        let host: String = mock_server.uri();
182
183        let result = migrate_db::<Task, OldTask>(&client, "test", &host).await;
184        println!("{:?}", result);
185
186        let box_result: Box<dyn Error> = result.unwrap_err();
187        let result: &DBMigrationError = box_result.downcast_ref().unwrap();
188        assert_eq!(
189            format!("{}", result),
190            format!(
191                "DBMigrationError: Error with the request to {}: error: \n{}",
192                format!("{}/{}/_bulk_docs", host, "test"),
193                error
194            )
195        )
196    }
197
198    // #[actix_rt::test]
199    // async fn migrate_db_test() {
200    //     let mock_server = MockServer::start().await;
201    //     Mock::given(method("PUT"))
202    //         .and(path("/test"))
203    //         .respond_with(ResponseTemplate::new(200))
204    //         .mount(&mock_server)
205    //         .await;
206    //
207    //     let client: Client = Client::default();
208    //     let host: String = mock_server.uri();
209    //     println!("test host: {}", host);
210    //     assert_eq!(migrate_db(&client, "test", &host).await.unwrap(), true);
211    // }
212}