autofill/db/
passports.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6use crate::db::{
7    models::{
8        passport::{InternalPassport, UpdatablePassportFields},
9        Metadata,
10    },
11    schema::{PASSPORT_COMMON_COLS, PASSPORT_COMMON_VALS},
12};
13use crate::error::*;
14
15use rusqlite::{Connection, Transaction};
16use sync_guid::Guid;
17use types::Timestamp;
18
19pub(crate) fn add_passport(
20    conn: &Connection,
21    new: UpdatablePassportFields,
22) -> Result<InternalPassport> {
23    let tx = conn.unchecked_transaction()?;
24    let now = Timestamp::now();
25
26    let passport = InternalPassport {
27        guid: Guid::random(),
28        name: new.name,
29        country: new.country,
30        passport_number: new.passport_number,
31        issue_date_month: new.issue_date_month,
32        issue_date_day: new.issue_date_day,
33        issue_date_year: new.issue_date_year,
34        expiry_date_month: new.expiry_date_month,
35        expiry_date_day: new.expiry_date_day,
36        expiry_date_year: new.expiry_date_year,
37        metadata: Metadata {
38            time_created: now,
39            time_last_modified: now,
40            ..Default::default()
41        },
42    };
43    add_internal_passport(&tx, &passport)?;
44    tx.commit()?;
45    Ok(passport)
46}
47
48fn add_internal_passport(tx: &Transaction<'_>, passport: &InternalPassport) -> Result<()> {
49    tx.execute(
50        &format!(
51            "INSERT INTO passports_data (
52                {common_cols},
53                sync_change_counter
54            ) VALUES (
55                {common_vals},
56                :sync_change_counter
57            )",
58            common_cols = PASSPORT_COMMON_COLS,
59            common_vals = PASSPORT_COMMON_VALS,
60        ),
61        rusqlite::named_params! {
62            ":guid": passport.guid,
63            ":name": passport.name,
64            ":country": passport.country,
65            ":passport_number": passport.passport_number,
66            ":issue_date_month": passport.issue_date_month,
67            ":issue_date_day": passport.issue_date_day,
68            ":issue_date_year": passport.issue_date_year,
69            ":expiry_date_month": passport.expiry_date_month,
70            ":expiry_date_day": passport.expiry_date_day,
71            ":expiry_date_year": passport.expiry_date_year,
72            ":time_created": passport.metadata.time_created,
73            ":time_last_used": passport.metadata.time_last_used,
74            ":time_last_modified": passport.metadata.time_last_modified,
75            ":times_used": passport.metadata.times_used,
76            ":sync_change_counter": passport.metadata.sync_change_counter,
77        },
78    )?;
79    Ok(())
80}
81
82pub(crate) fn get_passport(conn: &Connection, guid: &Guid) -> Result<InternalPassport> {
83    let sql = format!(
84        "SELECT
85            {common_cols},
86            sync_change_counter
87        FROM passports_data
88        WHERE guid = :guid",
89        common_cols = PASSPORT_COMMON_COLS
90    );
91    conn.query_row(&sql, [guid], InternalPassport::from_row)
92        .map_err(|e| match e {
93            rusqlite::Error::QueryReturnedNoRows => Error::NoSuchRecord(guid.to_string()),
94            e => e.into(),
95        })
96}
97
98pub(crate) fn get_all_passports(conn: &Connection) -> Result<Vec<InternalPassport>> {
99    let sql = format!(
100        "SELECT
101            {common_cols},
102            sync_change_counter
103        FROM passports_data",
104        common_cols = PASSPORT_COMMON_COLS
105    );
106    let mut stmt = conn.prepare(&sql)?;
107    let passports = stmt
108        .query_map([], InternalPassport::from_row)?
109        .collect::<std::result::Result<Vec<InternalPassport>, _>>()?;
110    Ok(passports)
111}
112
113pub(crate) fn count_all_passports(conn: &Connection) -> Result<i64> {
114    let sql = "SELECT COUNT(*) FROM passports_data";
115    let mut stmt = conn.prepare(sql)?;
116    let count: i64 = stmt.query_row([], |row| row.get(0))?;
117    Ok(count)
118}
119
120/// Updates just the "updatable" columns - suitable for exposure as a public
121/// API.
122pub(crate) fn update_passport(
123    conn: &Connection,
124    guid: &Guid,
125    passport: &UpdatablePassportFields,
126) -> Result<()> {
127    let tx = conn.unchecked_transaction()?;
128    tx.execute(
129        "UPDATE passports_data
130        SET name               = :name,
131            country            = :country,
132            passport_number    = :passport_number,
133            issue_date_month   = :issue_date_month,
134            issue_date_day     = :issue_date_day,
135            issue_date_year    = :issue_date_year,
136            expiry_date_month  = :expiry_date_month,
137            expiry_date_day    = :expiry_date_day,
138            expiry_date_year   = :expiry_date_year,
139            time_last_modified = :time_last_modified,
140            sync_change_counter = sync_change_counter + 1
141        WHERE guid             = :guid",
142        rusqlite::named_params! {
143            ":name": passport.name,
144            ":country": passport.country,
145            ":passport_number": passport.passport_number,
146            ":issue_date_month": passport.issue_date_month,
147            ":issue_date_day": passport.issue_date_day,
148            ":issue_date_year": passport.issue_date_year,
149            ":expiry_date_month": passport.expiry_date_month,
150            ":expiry_date_day": passport.expiry_date_day,
151            ":expiry_date_year": passport.expiry_date_year,
152            ":time_last_modified": Timestamp::now(),
153            ":guid": guid,
154        },
155    )?;
156    tx.commit()?;
157    Ok(())
158}
159
160pub(crate) fn delete_passport(conn: &Connection, guid: &Guid) -> Result<bool> {
161    let tx = conn.unchecked_transaction()?;
162    // execute returns how many rows were affected.
163    let exists = tx.execute(
164        "DELETE FROM passports_data WHERE guid = :guid",
165        rusqlite::named_params! { ":guid": guid },
166    )? != 0;
167    tx.commit()?;
168    Ok(exists)
169}
170
171pub fn touch(conn: &Connection, guid: &Guid) -> Result<()> {
172    let tx = conn.unchecked_transaction()?;
173    let now_ms = Timestamp::now();
174    tx.execute(
175        "UPDATE passports_data
176        SET time_last_used = :time_last_used,
177            times_used     = times_used + 1,
178            sync_change_counter = sync_change_counter + 1
179        WHERE guid         = :guid",
180        rusqlite::named_params! {
181            ":time_last_used": now_ms,
182            ":guid": guid,
183        },
184    )?;
185    tx.commit()?;
186    Ok(())
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::db::test::new_mem_db;
193
194    fn sample_fields(name: &str, number: &str) -> UpdatablePassportFields {
195        UpdatablePassportFields {
196            name: name.to_string(),
197            country: "CA".to_string(),
198            passport_number: number.to_string(),
199            issue_date_month: 1,
200            issue_date_day: 15,
201            issue_date_year: 2020,
202            expiry_date_month: 1,
203            expiry_date_day: 15,
204            expiry_date_year: 2030,
205        }
206    }
207
208    #[test]
209    fn test_passport_create_and_read() -> Result<()> {
210        let db = new_mem_db();
211
212        let saved = add_passport(&db, sample_fields("Jane Doe", "X1234567"))?;
213
214        // add populated the guid and timestamps
215        assert_ne!(Guid::default(), saved.guid);
216        assert_ne!(0, saved.metadata.time_created.as_millis());
217        assert_ne!(0, saved.metadata.time_last_modified.as_millis());
218
219        let retrieved = get_passport(&db, &saved.guid)?;
220        assert_eq!(saved.guid, retrieved.guid);
221        assert_eq!(retrieved.name, "Jane Doe");
222        assert_eq!(retrieved.country, "CA");
223        assert_eq!(retrieved.passport_number, "X1234567");
224        assert_eq!(retrieved.issue_date_month, 1);
225        assert_eq!(retrieved.issue_date_day, 15);
226        assert_eq!(retrieved.issue_date_year, 2020);
227        assert_eq!(retrieved.expiry_date_year, 2030);
228
229        // deleting removes it
230        assert!(delete_passport(&db, &saved.guid)?);
231        assert!(get_passport(&db, &saved.guid).is_err());
232
233        Ok(())
234    }
235
236    #[test]
237    fn test_passport_missing_guid() {
238        let db = new_mem_db();
239        let guid = Guid::random();
240        let result = get_passport(&db, &guid);
241        assert_eq!(
242            result.unwrap_err().to_string(),
243            Error::NoSuchRecord(guid.to_string()).to_string()
244        );
245    }
246
247    #[test]
248    fn test_passport_read_all() -> Result<()> {
249        let db = new_mem_db();
250
251        let a = add_passport(&db, sample_fields("Jane Doe", "A1"))?;
252        let b = add_passport(&db, sample_fields("John Deer", "B2"))?;
253        let c = add_passport(&db, sample_fields("Abe Lincoln", "C3"))?;
254
255        assert!(delete_passport(&db, &c.guid)?);
256
257        let all = get_all_passports(&db)?;
258        assert_eq!(all.len(), 2);
259        assert_eq!(count_all_passports(&db)?, 2);
260
261        let guids = [all[0].guid.as_str(), all[1].guid.as_str()];
262        assert!(guids.contains(&a.guid.as_str()));
263        assert!(guids.contains(&b.guid.as_str()));
264
265        Ok(())
266    }
267
268    #[test]
269    fn test_passport_update() -> Result<()> {
270        let db = new_mem_db();
271        let saved = add_passport(&db, sample_fields("John Deer", "Z9"))?;
272        assert_eq!(saved.metadata.sync_change_counter, 0);
273
274        let mut fields = sample_fields("John Doe", "Z9");
275        fields.expiry_date_year = 2035;
276        update_passport(&db, &saved.guid, &fields)?;
277
278        let updated = get_passport(&db, &saved.guid)?;
279        assert_eq!(updated.name, "John Doe");
280        assert_eq!(updated.expiry_date_year, 2035);
281        // updating bumps the sync change counter
282        assert_eq!(updated.metadata.sync_change_counter, 1);
283
284        Ok(())
285    }
286
287    #[test]
288    fn test_passport_delete() -> Result<()> {
289        let db = new_mem_db();
290        let saved = add_passport(&db, sample_fields("Jane Doe", "D1"))?;
291
292        assert!(delete_passport(&db, &saved.guid)?);
293        // deleting a non-existent record returns false
294        assert!(!delete_passport(&db, &saved.guid)?);
295
296        Ok(())
297    }
298
299    #[test]
300    fn test_passport_touch() -> Result<()> {
301        let db = new_mem_db();
302        let saved = add_passport(&db, sample_fields("Jane Doe", "T1"))?;
303        assert_eq!(saved.metadata.times_used, 0);
304        assert_eq!(saved.metadata.sync_change_counter, 0);
305
306        touch(&db, &saved.guid)?;
307
308        let touched = get_passport(&db, &saved.guid)?;
309        assert_eq!(touched.metadata.times_used, 1);
310        assert!(touched.metadata.time_last_used.as_millis() > 0);
311        // touching bumps the sync change counter
312        assert_eq!(touched.metadata.sync_change_counter, 1);
313
314        Ok(())
315    }
316}