1use std::sync::{Arc, Mutex};
12
13use pylon_auth::org::{Invite, Membership, Org, OrgBackend, OrgRole};
14use rusqlite::Connection;
15
16const ORGS_TABLE: &str = "_pylon_orgs";
17const MEMBERS_TABLE: &str = "_pylon_org_members";
18const INVITES_TABLE: &str = "_pylon_org_invites";
19
20pub struct SqliteOrgBackend {
25 conn: Arc<Mutex<Connection>>,
26}
27
28impl SqliteOrgBackend {
29 pub fn open(path: &str) -> Result<Self, String> {
30 let conn = Connection::open(path).map_err(|e| format!("open: {e}"))?;
31 Self::from_connection(conn)
32 }
33 pub fn in_memory() -> Result<Self, String> {
34 let conn = Connection::open_in_memory().map_err(|e| format!("open: {e}"))?;
35 Self::from_connection(conn)
36 }
37 fn from_connection(conn: Connection) -> Result<Self, String> {
38 conn.execute_batch(&format!(
39 "CREATE TABLE IF NOT EXISTS {ORGS_TABLE} (
40 id TEXT PRIMARY KEY,
41 name TEXT NOT NULL,
42 created_by TEXT NOT NULL,
43 created_at INTEGER NOT NULL
44 );
45 CREATE TABLE IF NOT EXISTS {MEMBERS_TABLE} (
46 org_id TEXT NOT NULL,
47 user_id TEXT NOT NULL,
48 role TEXT NOT NULL,
49 joined_at INTEGER NOT NULL,
50 PRIMARY KEY (org_id, user_id)
51 );
52 CREATE INDEX IF NOT EXISTS {MEMBERS_TABLE}_user_idx ON {MEMBERS_TABLE}(user_id);
53 CREATE TABLE IF NOT EXISTS {INVITES_TABLE} (
54 id TEXT PRIMARY KEY,
55 org_id TEXT NOT NULL,
56 email TEXT NOT NULL,
57 role TEXT NOT NULL,
58 invited_by TEXT NOT NULL,
59 token_hash TEXT NOT NULL,
60 token_prefix TEXT NOT NULL,
61 created_at INTEGER NOT NULL,
62 expires_at INTEGER NOT NULL,
63 accepted_at INTEGER
64 );
65 CREATE INDEX IF NOT EXISTS {INVITES_TABLE}_prefix_idx ON {INVITES_TABLE}(token_prefix);
66 CREATE INDEX IF NOT EXISTS {INVITES_TABLE}_org_idx ON {INVITES_TABLE}(org_id);"
67 ))
68 .map_err(|e| format!("init schema: {e}"))?;
69 Ok(Self {
70 conn: Arc::new(Mutex::new(conn)),
71 })
72 }
73}
74
75fn role_to_str(r: OrgRole) -> &'static str {
76 r.as_str()
77}
78
79fn role_from_str(s: &str) -> OrgRole {
80 OrgRole::from_str(s).unwrap_or(OrgRole::Member)
81}
82
83impl OrgBackend for SqliteOrgBackend {
84 fn put_org(&self, org: &Org) {
85 if let Ok(c) = self.conn.lock() {
86 let _ = c.execute(
87 &format!(
88 "INSERT INTO {ORGS_TABLE} (id, name, created_by, created_at)
89 VALUES (?1, ?2, ?3, ?4)
90 ON CONFLICT(id) DO UPDATE SET
91 name = excluded.name"
92 ),
93 rusqlite::params![org.id, org.name, org.created_by, org.created_at as i64],
94 );
95 }
96 }
97
98 fn get_org(&self, id: &str) -> Option<Org> {
99 let c = self.conn.lock().ok()?;
100 c.query_row(
101 &format!("SELECT id, name, created_by, created_at FROM {ORGS_TABLE} WHERE id = ?1"),
102 rusqlite::params![id],
103 |r| {
104 Ok(Org {
105 id: r.get(0)?,
106 name: r.get(1)?,
107 created_by: r.get(2)?,
108 created_at: r.get::<_, i64>(3)? as u64,
109 })
110 },
111 )
112 .ok()
113 }
114
115 fn delete_org(&self, id: &str) -> bool {
116 let Ok(c) = self.conn.lock() else {
117 return false;
118 };
119 let _ = c.execute(
122 &format!("DELETE FROM {MEMBERS_TABLE} WHERE org_id = ?1"),
123 rusqlite::params![id],
124 );
125 let _ = c.execute(
126 &format!("DELETE FROM {INVITES_TABLE} WHERE org_id = ?1"),
127 rusqlite::params![id],
128 );
129 c.execute(
130 &format!("DELETE FROM {ORGS_TABLE} WHERE id = ?1"),
131 rusqlite::params![id],
132 )
133 .map(|n| n > 0)
134 .unwrap_or(false)
135 }
136
137 fn list_orgs_for_user(&self, user_id: &str) -> Vec<(Org, OrgRole)> {
138 let Ok(c) = self.conn.lock() else {
139 return vec![];
140 };
141 let mut stmt = match c.prepare(&format!(
142 "SELECT o.id, o.name, o.created_by, o.created_at, m.role
143 FROM {ORGS_TABLE} o JOIN {MEMBERS_TABLE} m ON o.id = m.org_id
144 WHERE m.user_id = ?1
145 ORDER BY o.created_at DESC"
146 )) {
147 Ok(s) => s,
148 Err(_) => return vec![],
149 };
150 let iter = match stmt.query_map(rusqlite::params![user_id], |r| {
151 let role: String = r.get(4)?;
152 Ok((
153 Org {
154 id: r.get(0)?,
155 name: r.get(1)?,
156 created_by: r.get(2)?,
157 created_at: r.get::<_, i64>(3)? as u64,
158 },
159 role_from_str(&role),
160 ))
161 }) {
162 Ok(it) => it,
163 Err(_) => return vec![],
164 };
165 iter.filter_map(|r| r.ok()).collect()
166 }
167
168 fn put_membership(&self, m: &Membership) {
169 if let Ok(c) = self.conn.lock() {
170 let _ = c.execute(
171 &format!(
172 "INSERT INTO {MEMBERS_TABLE} (org_id, user_id, role, joined_at)
173 VALUES (?1, ?2, ?3, ?4)
174 ON CONFLICT(org_id, user_id) DO UPDATE SET role = excluded.role"
175 ),
176 rusqlite::params![m.org_id, m.user_id, role_to_str(m.role), m.joined_at as i64],
177 );
178 }
179 }
180
181 fn get_membership(&self, org_id: &str, user_id: &str) -> Option<Membership> {
182 let c = self.conn.lock().ok()?;
183 c.query_row(
184 &format!(
185 "SELECT org_id, user_id, role, joined_at FROM {MEMBERS_TABLE}
186 WHERE org_id = ?1 AND user_id = ?2"
187 ),
188 rusqlite::params![org_id, user_id],
189 |r| {
190 let role: String = r.get(2)?;
191 Ok(Membership {
192 org_id: r.get(0)?,
193 user_id: r.get(1)?,
194 role: role_from_str(&role),
195 joined_at: r.get::<_, i64>(3)? as u64,
196 })
197 },
198 )
199 .ok()
200 }
201
202 fn delete_membership(&self, org_id: &str, user_id: &str) -> bool {
203 let Ok(c) = self.conn.lock() else {
204 return false;
205 };
206 c.execute(
207 &format!("DELETE FROM {MEMBERS_TABLE} WHERE org_id = ?1 AND user_id = ?2"),
208 rusqlite::params![org_id, user_id],
209 )
210 .map(|n| n > 0)
211 .unwrap_or(false)
212 }
213
214 fn list_members(&self, org_id: &str) -> Vec<Membership> {
215 let Ok(c) = self.conn.lock() else {
216 return vec![];
217 };
218 let mut stmt = match c.prepare(&format!(
219 "SELECT org_id, user_id, role, joined_at FROM {MEMBERS_TABLE}
220 WHERE org_id = ?1 ORDER BY joined_at"
221 )) {
222 Ok(s) => s,
223 Err(_) => return vec![],
224 };
225 let iter = match stmt.query_map(rusqlite::params![org_id], |r| {
226 let role: String = r.get(2)?;
227 Ok(Membership {
228 org_id: r.get(0)?,
229 user_id: r.get(1)?,
230 role: role_from_str(&role),
231 joined_at: r.get::<_, i64>(3)? as u64,
232 })
233 }) {
234 Ok(it) => it,
235 Err(_) => return vec![],
236 };
237 iter.filter_map(|r| r.ok()).collect()
238 }
239
240 fn put_invite(&self, inv: &Invite) {
241 if let Ok(c) = self.conn.lock() {
242 let _ = c.execute(
243 &format!(
244 "INSERT INTO {INVITES_TABLE}
245 (id, org_id, email, role, invited_by, token_hash, token_prefix,
246 created_at, expires_at, accepted_at)
247 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
248 ON CONFLICT(id) DO UPDATE SET
249 accepted_at = excluded.accepted_at"
250 ),
251 rusqlite::params![
252 inv.id,
253 inv.org_id,
254 inv.email,
255 role_to_str(inv.role),
256 inv.invited_by,
257 inv.token_hash,
258 inv.token_prefix,
259 inv.created_at as i64,
260 inv.expires_at as i64,
261 inv.accepted_at.map(|v| v as i64),
262 ],
263 );
264 }
265 }
266
267 fn get_invite(&self, id: &str) -> Option<Invite> {
268 let c = self.conn.lock().ok()?;
269 c.query_row(
270 &format!(
271 "SELECT id, org_id, email, role, invited_by, token_hash, token_prefix,
272 created_at, expires_at, accepted_at
273 FROM {INVITES_TABLE} WHERE id = ?1"
274 ),
275 rusqlite::params![id],
276 row_to_invite,
277 )
278 .ok()
279 }
280
281 fn list_invites(&self, org_id: &str) -> Vec<Invite> {
282 let Ok(c) = self.conn.lock() else {
283 return vec![];
284 };
285 let mut stmt = match c.prepare(&format!(
286 "SELECT id, org_id, email, role, invited_by, token_hash, token_prefix,
287 created_at, expires_at, accepted_at
288 FROM {INVITES_TABLE}
289 WHERE org_id = ?1 AND accepted_at IS NULL
290 ORDER BY created_at DESC"
291 )) {
292 Ok(s) => s,
293 Err(_) => return vec![],
294 };
295 let iter = match stmt.query_map(rusqlite::params![org_id], row_to_invite) {
296 Ok(it) => it,
297 Err(_) => return vec![],
298 };
299 iter.filter_map(|r| r.ok()).collect()
300 }
301
302 fn delete_invite(&self, id: &str) -> bool {
303 let Ok(c) = self.conn.lock() else {
304 return false;
305 };
306 c.execute(
307 &format!("DELETE FROM {INVITES_TABLE} WHERE id = ?1"),
308 rusqlite::params![id],
309 )
310 .map(|n| n > 0)
311 .unwrap_or(false)
312 }
313
314 fn invites_by_prefix(&self, prefix: &str) -> Vec<Invite> {
315 let Ok(c) = self.conn.lock() else {
316 return vec![];
317 };
318 let mut stmt = match c.prepare(&format!(
321 "SELECT id, org_id, email, role, invited_by, token_hash, token_prefix,
322 created_at, expires_at, accepted_at
323 FROM {INVITES_TABLE} WHERE token_prefix = ?1"
324 )) {
325 Ok(s) => s,
326 Err(_) => return vec![],
327 };
328 let iter = match stmt.query_map(rusqlite::params![prefix], row_to_invite) {
329 Ok(it) => it,
330 Err(_) => return vec![],
331 };
332 iter.filter_map(|r| r.ok()).collect()
333 }
334}
335
336fn row_to_invite(row: &rusqlite::Row<'_>) -> rusqlite::Result<Invite> {
337 let role: String = row.get(3)?;
338 Ok(Invite {
339 id: row.get(0)?,
340 org_id: row.get(1)?,
341 email: row.get(2)?,
342 role: role_from_str(&role),
343 invited_by: row.get(4)?,
344 token_hash: row.get(5)?,
345 token_prefix: row.get(6)?,
346 created_at: row.get::<_, i64>(7)? as u64,
347 expires_at: row.get::<_, i64>(8)? as u64,
348 accepted_at: row.get::<_, Option<i64>>(9)?.map(|v| v as u64),
349 })
350}
351
352pub use pg::PostgresOrgBackend;
357
358mod pg {
359 use super::*;
360 use postgres::Client;
361
362 pub struct PostgresOrgBackend {
363 client: Mutex<Client>,
364 }
365
366 impl PostgresOrgBackend {
367 pub fn connect(url: &str) -> Result<Self, String> {
368 let mut client =
369 Client::connect(url, postgres::NoTls).map_err(|e| format!("PG connect: {e}"))?;
370 client
371 .batch_execute(&format!(
372 "CREATE TABLE IF NOT EXISTS {ORGS_TABLE} (
373 id TEXT PRIMARY KEY,
374 name TEXT NOT NULL,
375 created_by TEXT NOT NULL,
376 created_at BIGINT NOT NULL
377 );
378 CREATE TABLE IF NOT EXISTS {MEMBERS_TABLE} (
379 org_id TEXT NOT NULL,
380 user_id TEXT NOT NULL,
381 role TEXT NOT NULL,
382 joined_at BIGINT NOT NULL,
383 PRIMARY KEY (org_id, user_id)
384 );
385 CREATE INDEX IF NOT EXISTS {MEMBERS_TABLE}_user_idx ON {MEMBERS_TABLE}(user_id);
386 CREATE TABLE IF NOT EXISTS {INVITES_TABLE} (
387 id TEXT PRIMARY KEY,
388 org_id TEXT NOT NULL,
389 email TEXT NOT NULL,
390 role TEXT NOT NULL,
391 invited_by TEXT NOT NULL,
392 token_hash TEXT NOT NULL,
393 token_prefix TEXT NOT NULL,
394 created_at BIGINT NOT NULL,
395 expires_at BIGINT NOT NULL,
396 accepted_at BIGINT
397 );
398 CREATE INDEX IF NOT EXISTS {INVITES_TABLE}_prefix_idx ON {INVITES_TABLE}(token_prefix);
399 CREATE INDEX IF NOT EXISTS {INVITES_TABLE}_org_idx ON {INVITES_TABLE}(org_id);"
400 ))
401 .map_err(|e| format!("PG init schema: {e}"))?;
402 Ok(Self {
403 client: Mutex::new(client),
404 })
405 }
406 }
407
408 impl OrgBackend for PostgresOrgBackend {
409 fn put_org(&self, org: &Org) {
410 if let Ok(mut c) = self.client.lock() {
411 let _ = c.execute(
412 &format!(
413 "INSERT INTO {ORGS_TABLE} (id, name, created_by, created_at)
414 VALUES ($1, $2, $3, $4)
415 ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name"
416 ),
417 &[&org.id, &org.name, &org.created_by, &(org.created_at as i64)],
418 );
419 }
420 }
421
422 fn get_org(&self, id: &str) -> Option<Org> {
423 let mut c = self.client.lock().ok()?;
424 let row = c
425 .query_opt(
426 &format!(
427 "SELECT id, name, created_by, created_at FROM {ORGS_TABLE} WHERE id = $1"
428 ),
429 &[&id],
430 )
431 .ok()??;
432 Some(Org {
433 id: row.get(0),
434 name: row.get(1),
435 created_by: row.get(2),
436 created_at: row.get::<_, i64>(3) as u64,
437 })
438 }
439
440 fn delete_org(&self, id: &str) -> bool {
441 let Ok(mut c) = self.client.lock() else {
442 return false;
443 };
444 let _ = c.execute(
445 &format!("DELETE FROM {MEMBERS_TABLE} WHERE org_id = $1"),
446 &[&id],
447 );
448 let _ = c.execute(
449 &format!("DELETE FROM {INVITES_TABLE} WHERE org_id = $1"),
450 &[&id],
451 );
452 c.execute(
453 &format!("DELETE FROM {ORGS_TABLE} WHERE id = $1"),
454 &[&id],
455 )
456 .map(|n| n > 0)
457 .unwrap_or(false)
458 }
459
460 fn list_orgs_for_user(&self, user_id: &str) -> Vec<(Org, OrgRole)> {
461 let Ok(mut c) = self.client.lock() else {
462 return vec![];
463 };
464 let rows = match c.query(
465 &format!(
466 "SELECT o.id, o.name, o.created_by, o.created_at, m.role
467 FROM {ORGS_TABLE} o JOIN {MEMBERS_TABLE} m ON o.id = m.org_id
468 WHERE m.user_id = $1
469 ORDER BY o.created_at DESC"
470 ),
471 &[&user_id],
472 ) {
473 Ok(r) => r,
474 Err(_) => return vec![],
475 };
476 rows.iter()
477 .map(|row| {
478 let role: String = row.get(4);
479 (
480 Org {
481 id: row.get(0),
482 name: row.get(1),
483 created_by: row.get(2),
484 created_at: row.get::<_, i64>(3) as u64,
485 },
486 role_from_str(&role),
487 )
488 })
489 .collect()
490 }
491
492 fn put_membership(&self, m: &Membership) {
493 if let Ok(mut c) = self.client.lock() {
494 let _ = c.execute(
495 &format!(
496 "INSERT INTO {MEMBERS_TABLE} (org_id, user_id, role, joined_at)
497 VALUES ($1, $2, $3, $4)
498 ON CONFLICT (org_id, user_id) DO UPDATE SET role = EXCLUDED.role"
499 ),
500 &[&m.org_id, &m.user_id, &role_to_str(m.role), &(m.joined_at as i64)],
501 );
502 }
503 }
504
505 fn get_membership(&self, org_id: &str, user_id: &str) -> Option<Membership> {
506 let mut c = self.client.lock().ok()?;
507 let row = c
508 .query_opt(
509 &format!(
510 "SELECT org_id, user_id, role, joined_at FROM {MEMBERS_TABLE}
511 WHERE org_id = $1 AND user_id = $2"
512 ),
513 &[&org_id, &user_id],
514 )
515 .ok()??;
516 let role: String = row.get(2);
517 Some(Membership {
518 org_id: row.get(0),
519 user_id: row.get(1),
520 role: role_from_str(&role),
521 joined_at: row.get::<_, i64>(3) as u64,
522 })
523 }
524
525 fn delete_membership(&self, org_id: &str, user_id: &str) -> bool {
526 let Ok(mut c) = self.client.lock() else {
527 return false;
528 };
529 c.execute(
530 &format!("DELETE FROM {MEMBERS_TABLE} WHERE org_id = $1 AND user_id = $2"),
531 &[&org_id, &user_id],
532 )
533 .map(|n| n > 0)
534 .unwrap_or(false)
535 }
536
537 fn list_members(&self, org_id: &str) -> Vec<Membership> {
538 let Ok(mut c) = self.client.lock() else {
539 return vec![];
540 };
541 let rows = match c.query(
542 &format!(
543 "SELECT org_id, user_id, role, joined_at FROM {MEMBERS_TABLE}
544 WHERE org_id = $1 ORDER BY joined_at"
545 ),
546 &[&org_id],
547 ) {
548 Ok(r) => r,
549 Err(_) => return vec![],
550 };
551 rows.iter()
552 .map(|row| {
553 let role: String = row.get(2);
554 Membership {
555 org_id: row.get(0),
556 user_id: row.get(1),
557 role: role_from_str(&role),
558 joined_at: row.get::<_, i64>(3) as u64,
559 }
560 })
561 .collect()
562 }
563
564 fn put_invite(&self, inv: &Invite) {
565 if let Ok(mut c) = self.client.lock() {
566 let _ = c.execute(
567 &format!(
568 "INSERT INTO {INVITES_TABLE}
569 (id, org_id, email, role, invited_by, token_hash, token_prefix,
570 created_at, expires_at, accepted_at)
571 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
572 ON CONFLICT (id) DO UPDATE SET accepted_at = EXCLUDED.accepted_at"
573 ),
574 &[
575 &inv.id,
576 &inv.org_id,
577 &inv.email,
578 &role_to_str(inv.role),
579 &inv.invited_by,
580 &inv.token_hash,
581 &inv.token_prefix,
582 &(inv.created_at as i64),
583 &(inv.expires_at as i64),
584 &inv.accepted_at.map(|v| v as i64),
585 ],
586 );
587 }
588 }
589
590 fn get_invite(&self, id: &str) -> Option<Invite> {
591 let mut c = self.client.lock().ok()?;
592 let row = c
593 .query_opt(
594 &format!(
595 "SELECT id, org_id, email, role, invited_by, token_hash, token_prefix,
596 created_at, expires_at, accepted_at
597 FROM {INVITES_TABLE} WHERE id = $1"
598 ),
599 &[&id],
600 )
601 .ok()??;
602 Some(pg_row_to_invite(&row))
603 }
604
605 fn list_invites(&self, org_id: &str) -> Vec<Invite> {
606 let Ok(mut c) = self.client.lock() else {
607 return vec![];
608 };
609 let rows = match c.query(
610 &format!(
611 "SELECT id, org_id, email, role, invited_by, token_hash, token_prefix,
612 created_at, expires_at, accepted_at
613 FROM {INVITES_TABLE}
614 WHERE org_id = $1 AND accepted_at IS NULL
615 ORDER BY created_at DESC"
616 ),
617 &[&org_id],
618 ) {
619 Ok(r) => r,
620 Err(_) => return vec![],
621 };
622 rows.iter().map(pg_row_to_invite).collect()
623 }
624
625 fn delete_invite(&self, id: &str) -> bool {
626 let Ok(mut c) = self.client.lock() else {
627 return false;
628 };
629 c.execute(
630 &format!("DELETE FROM {INVITES_TABLE} WHERE id = $1"),
631 &[&id],
632 )
633 .map(|n| n > 0)
634 .unwrap_or(false)
635 }
636
637 fn invites_by_prefix(&self, prefix: &str) -> Vec<Invite> {
638 let Ok(mut c) = self.client.lock() else {
639 return vec![];
640 };
641 let rows = match c.query(
642 &format!(
643 "SELECT id, org_id, email, role, invited_by, token_hash, token_prefix,
644 created_at, expires_at, accepted_at
645 FROM {INVITES_TABLE} WHERE token_prefix = $1"
646 ),
647 &[&prefix],
648 ) {
649 Ok(r) => r,
650 Err(_) => return vec![],
651 };
652 rows.iter().map(pg_row_to_invite).collect()
653 }
654 }
655
656 fn pg_row_to_invite(row: &postgres::Row) -> Invite {
657 let role: String = row.get(3);
658 Invite {
659 id: row.get(0),
660 org_id: row.get(1),
661 email: row.get(2),
662 role: role_from_str(&role),
663 invited_by: row.get(4),
664 token_hash: row.get(5),
665 token_prefix: row.get(6),
666 created_at: row.get::<_, i64>(7) as u64,
667 expires_at: row.get::<_, i64>(8) as u64,
668 accepted_at: row.get::<_, Option<i64>>(9).map(|v| v as u64),
669 }
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use pylon_auth::org::{Invite, Membership, Org, OrgRole};
677
678 #[test]
679 fn sqlite_org_round_trip() {
680 let b = SqliteOrgBackend::in_memory().unwrap();
681 let org = Org {
682 id: "o1".into(),
683 name: "Acme".into(),
684 created_by: "u1".into(),
685 created_at: 100,
686 };
687 b.put_org(&org);
688 assert_eq!(b.get_org("o1").unwrap().name, "Acme");
689 assert!(b.delete_org("o1"));
690 assert!(b.get_org("o1").is_none());
691 }
692
693 #[test]
694 fn sqlite_membership_and_list_for_user() {
695 let b = SqliteOrgBackend::in_memory().unwrap();
696 b.put_org(&Org {
697 id: "o1".into(),
698 name: "A".into(),
699 created_by: "u1".into(),
700 created_at: 100,
701 });
702 b.put_org(&Org {
703 id: "o2".into(),
704 name: "B".into(),
705 created_by: "u2".into(),
706 created_at: 200,
707 });
708 b.put_membership(&Membership {
709 org_id: "o1".into(),
710 user_id: "u1".into(),
711 role: OrgRole::Owner,
712 joined_at: 100,
713 });
714 b.put_membership(&Membership {
715 org_id: "o2".into(),
716 user_id: "u1".into(),
717 role: OrgRole::Member,
718 joined_at: 200,
719 });
720 let list = b.list_orgs_for_user("u1");
721 assert_eq!(list.len(), 2);
722 assert_eq!(list[0].0.id, "o2");
724 assert_eq!(list[0].1, OrgRole::Member);
725 }
726
727 #[test]
728 fn sqlite_invite_prefix_index_used() {
729 let b = SqliteOrgBackend::in_memory().unwrap();
730 b.put_org(&Org {
731 id: "o1".into(),
732 name: "A".into(),
733 created_by: "u1".into(),
734 created_at: 100,
735 });
736 b.put_invite(&Invite {
737 id: "i1".into(),
738 org_id: "o1".into(),
739 email: "x@y.com".into(),
740 role: OrgRole::Member,
741 invited_by: "u1".into(),
742 token_hash: "h".into(),
743 token_prefix: "abcd1234".into(),
744 created_at: 100,
745 expires_at: 9_999_999_999,
746 accepted_at: None,
747 });
748 let hits = b.invites_by_prefix("abcd1234");
749 assert_eq!(hits.len(), 1);
750 let misses = b.invites_by_prefix("nomatch1");
751 assert_eq!(misses.len(), 0);
752 }
753
754 #[test]
755 fn sqlite_delete_org_cascades() {
756 let b = SqliteOrgBackend::in_memory().unwrap();
757 b.put_org(&Org {
758 id: "o1".into(),
759 name: "A".into(),
760 created_by: "u1".into(),
761 created_at: 100,
762 });
763 b.put_membership(&Membership {
764 org_id: "o1".into(),
765 user_id: "u1".into(),
766 role: OrgRole::Owner,
767 joined_at: 100,
768 });
769 b.put_invite(&Invite {
770 id: "i1".into(),
771 org_id: "o1".into(),
772 email: "x@y.com".into(),
773 role: OrgRole::Member,
774 invited_by: "u1".into(),
775 token_hash: "h".into(),
776 token_prefix: "p".into(),
777 created_at: 100,
778 expires_at: 9_999_999_999,
779 accepted_at: None,
780 });
781 assert!(b.delete_org("o1"));
782 assert!(b.list_members("o1").is_empty());
783 assert!(b.list_invites("o1").is_empty());
784 }
785}