1use 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
120pub(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 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 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 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 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 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 assert_eq!(touched.metadata.sync_change_counter, 1);
313
314 Ok(())
315 }
316}