Compare commits

...

11 Commits

Author SHA1 Message Date
Lennart
eba377b980 update dependencies 2025-12-05 11:47:11 +01:00
Lennart
54f1ee0788 use similar-asserts for regression tests 2025-11-22 18:46:47 +01:00
Lennart
22a0337375 version 0.10.5 2025-11-17 19:14:17 +01:00
Lennart
21902e108a fix some error messages 2025-11-17 19:13:13 +01:00
Lennart
08f526fa5b Add startup routine to fix orphaned objects
fixes #145, related to #142
2025-11-17 19:11:30 +01:00
Lennart
ac73f3aaff addressbook_store: Commit import addressbooks to changelog 2025-11-17 18:35:10 +01:00
Lennart
9fdc8434db calendar import: log added events 2025-11-17 18:22:33 +01:00
Lennart
85f3d89235 version 0.10.4 2025-11-17 01:21:55 +01:00
Lennart
092604694a multiget: percent-decode hrefs 2025-11-17 01:21:20 +01:00
Lennart
8ef24668ba version 0.10.3 2025-11-14 11:02:27 +01:00
Lennart
416658d069 frontend: Fix missing getTimezones import in create-calendar-form
fixes #141
2025-11-14 11:01:59 +01:00
15 changed files with 549 additions and 231 deletions

View File

@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM calendarobjects\n WHERE (principal, cal_id, id) NOT IN (\n SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog\n )\n ;\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "cal_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "deleted: bool",
"ordinal": 3,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM addressobjects\n WHERE (principal, addressbook_id, id) NOT IN (\n SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog\n )\n ;\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "addressbook_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "deleted: bool",
"ordinal": 3,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d"
}

422
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.10.2" version = "0.10.5"
rust-version = "1.91" rust-version = "1.91"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
@@ -133,7 +133,7 @@ syn = { version = "2.0", features = ["full"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
heck = "0.5" heck = "0.5"
darling = "0.21" darling = "0.23"
reqwest = { version = "0.12", features = [ reqwest = { version = "0.12", features = [
"rustls-tls", "rustls-tls",
"charset", "charset",
@@ -148,6 +148,7 @@ ece = { version = "2.3", default-features = false, features = [
] } ] }
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
async-std = { version = "1.13", features = ["attributes"] } async-std = { version = "1.13", features = ["attributes"] }
similar-asserts = "1.7"
[dependencies] [dependencies]
rustical_store.workspace = true rustical_store.workspace = true

View File

@@ -45,3 +45,4 @@ tower-http.workspace = true
strum.workspace = true strum.workspace = true
strum_macros.workspace = true strum_macros.workspace = true
vtimezones-rs.workspace = true vtimezones-rs.workspace = true
similar-asserts.workspace = true

View File

@@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
let mut not_found = vec![]; let mut not_found = vec![];
for href in &cal_query.href { for href in &cal_query.href {
if let Some(filename) = href.strip_prefix(path) { if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
&& let Some(filename) = href.strip_prefix(path)
{
let filename = filename.trim_start_matches('/'); let filename = filename.trim_start_matches('/');
if let Some(object_id) = filename.strip_suffix(".ics") { if let Some(object_id) = filename.strip_suffix(".ics") {
match store.get_object(principal, cal_id, object_id, false).await { match store.get_object(principal, cal_id, object_id, false).await {
Ok(object) => result.push(object), Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_string());
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());

View File

@@ -39,9 +39,7 @@ async fn test_propfind() {
.unwrap() .unwrap()
.trim() .trim()
.replace("\r\n", "\n"); .replace("\r\n", "\n");
println!("{output}"); similar_asserts::assert_eq!(output, expected_output);
println!("{}, {} \n\n\n", output.len(), expected_output.len());
assert_eq!(output, expected_output);
} }
} }
} }

View File

@@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
let mut not_found = vec![]; let mut not_found = vec![];
for href in &addressbook_multiget.href { for href in &addressbook_multiget.href {
if let Some(filename) = href.strip_prefix(path) { if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
&& let Some(filename) = href.strip_prefix(path)
{
let filename = filename.trim_start_matches('/'); let filename = filename.trim_start_matches('/');
if let Some(object_id) = filename.strip_suffix(".vcf") { if let Some(object_id) = filename.strip_suffix(".vcf") {
match store match store
@@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
.await .await
{ {
Ok(object) => result.push(object), Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_string());
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());

View File

@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from "."; import { escapeXml } from ".";
import { getTimezones } from "./timezones.ts";
@customElement("create-calendar-form") @customElement("create-calendar-form")
export class CreateCalendarForm extends LitElement { export class CreateCalendarForm extends LitElement {

View File

@@ -2,6 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs"; import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs"; import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {

View File

@@ -2,18 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs"; import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs"; import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
let timezonesPromise = null; import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
async function getTimezones() {
timezonesPromise ||= new Promise(async (resolve, reject) => {
try {
let response = await fetch("/frontend/_timezones.json");
resolve(await response.json());
} catch (e2) {
reject(e2);
}
});
return await timezonesPromise;
}
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {

View File

@@ -0,0 +1,15 @@
let timezonesPromise = null;
async function getTimezones() {
timezonesPromise ||= new Promise(async (resolve, reject) => {
try {
let response = await fetch("/frontend/_timezones.json");
resolve(await response.json());
} catch (e) {
reject(e);
}
});
return await timezonesPromise;
}
export {
getTimezones as g
};

View File

@@ -9,7 +9,7 @@ use rustical_store::{
}; };
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::{error, instrument}; use tracing::{error, instrument, warn};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct AddressObjectRow { struct AddressObjectRow {
@@ -32,6 +32,60 @@ pub struct SqliteAddressbookStore {
} }
impl SqliteAddressbookStore { impl SqliteAddressbookStore {
// Commit "orphaned" objects to the changelog table
pub async fn repair_orphans(&self) -> Result<(), Error> {
struct Row {
principal: String,
addressbook_id: String,
id: String,
deleted: bool,
}
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let rows = sqlx::query_as!(
Row,
r#"
SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
FROM addressobjects
WHERE (principal, addressbook_id, id) NOT IN (
SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog
)
;
"#,
)
.fetch_all(&mut *tx)
.await
.map_err(crate::Error::from)?;
for row in rows {
let operation = if row.deleted {
ChangeOperation::Delete
} else {
ChangeOperation::Add
};
warn!(
"Commiting orphaned addressbook object ({},{},{}), deleted={}",
&row.principal, &row.addressbook_id, &row.id, &row.deleted
);
log_object_operation(
&mut tx,
&row.principal,
&row.addressbook_id,
&row.id,
operation,
)
.await?;
}
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>( async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: &str, principal: &str,
@@ -90,9 +144,9 @@ impl SqliteAddressbookStore {
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>( async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: String, principal: &str,
id: String, id: &str,
addressbook: Addressbook, addressbook: &Addressbook,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
let result = sqlx::query!( let result = sqlx::query!(
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ? r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
@@ -116,7 +170,7 @@ impl SqliteAddressbookStore {
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>( async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
addressbook: Addressbook, addressbook: &Addressbook,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
sqlx::query!( sqlx::query!(
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic) r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
@@ -283,9 +337,9 @@ impl SqliteAddressbookStore {
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>( async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: String, principal: &str,
addressbook_id: String, addressbook_id: &str,
object: AddressObject, object: &AddressObject,
overwrite: bool, overwrite: bool,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
let (object_id, vcf) = (object.get_id(), object.get_vcf()); let (object_id, vcf) = (object.get_id(), object.get_vcf());
@@ -397,7 +451,7 @@ impl AddressbookStore for SqliteAddressbookStore {
id: String, id: String,
addressbook: Addressbook, addressbook: Addressbook,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
Self::_update_addressbook(&self.db, principal, id, addressbook).await Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
} }
#[instrument] #[instrument]
@@ -405,7 +459,7 @@ impl AddressbookStore for SqliteAddressbookStore {
&self, &self,
addressbook: Addressbook, addressbook: Addressbook,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
Self::_insert_addressbook(&self.db, addressbook).await Self::_insert_addressbook(&self.db, &addressbook).await
} }
#[instrument] #[instrument]
@@ -521,14 +575,7 @@ impl AddressbookStore for SqliteAddressbookStore {
let object_id = object.get_id().to_owned(); let object_id = object.get_id().to_owned();
Self::_put_object( Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?;
&mut *tx,
principal.clone(),
addressbook_id.clone(),
object,
overwrite,
)
.await?;
let sync_token = log_object_operation( let sync_token = log_object_operation(
&mut tx, &mut tx,
@@ -628,7 +675,7 @@ impl AddressbookStore for SqliteAddressbookStore {
.await? .await?
.push_topic, .push_topic,
}) { }) {
error!("Push notification about deleted addressbook failed: {err}"); error!("Push notification about restored addressbook object failed: {err}");
} }
Ok(()) Ok(())
@@ -659,21 +706,44 @@ impl AddressbookStore for SqliteAddressbookStore {
return Err(Error::AlreadyExists); return Err(Error::AlreadyExists);
} }
if existing.is_none() { if existing.is_none() {
Self::_insert_addressbook(&mut *tx, addressbook.clone()).await?; Self::_insert_addressbook(&mut *tx, &addressbook).await?;
} }
let mut sync_token = None;
for object in objects { for object in objects {
Self::_put_object( Self::_put_object(
&mut *tx, &mut *tx,
addressbook.principal.clone(), &addressbook.principal,
addressbook.id.clone(), &addressbook.id,
object, &object,
false, false,
) )
.await?; .await?;
sync_token = Some(
log_object_operation(
&mut tx,
&addressbook.principal,
&addressbook.id,
object.get_id(),
ChangeOperation::Add,
)
.await?,
);
} }
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Some(sync_token) = sync_token
&& let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self
.get_addressbook(&addressbook.principal, &addressbook.id, true)
.await?
.push_topic,
})
{
error!("Push notification about imported addressbook failed: {err}");
}
Ok(()) Ok(())
} }
} }
@@ -685,7 +755,7 @@ async fn log_object_operation(
addressbook_id: &str, addressbook_id: &str,
object_id: &str, object_id: &str,
operation: ChangeOperation, operation: ChangeOperation,
) -> Result<String, sqlx::Error> { ) -> Result<String, Error> {
struct Synctoken { struct Synctoken {
synctoken: i64, synctoken: i64,
} }
@@ -700,7 +770,8 @@ async fn log_object_operation(
addressbook_id addressbook_id
) )
.fetch_one(&mut **tx) .fetch_one(&mut **tx)
.await?; .await
.map_err(crate::Error::from)?;
sqlx::query!( sqlx::query!(
r#" r#"
@@ -714,6 +785,7 @@ async fn log_object_operation(
operation operation
) )
.execute(&mut **tx) .execute(&mut **tx)
.await?; .await
.map_err(crate::Error::from)?;
Ok(format_synctoken(synctoken)) Ok(format_synctoken(synctoken))
} }

View File

@@ -11,7 +11,7 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::{error, instrument}; use tracing::{error, instrument, warn};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct CalendarObjectRow { struct CalendarObjectRow {
@@ -94,6 +94,53 @@ pub struct SqliteCalendarStore {
} }
impl SqliteCalendarStore { impl SqliteCalendarStore {
// Commit "orphaned" objects to the changelog table
pub async fn repair_orphans(&self) -> Result<(), Error> {
struct Row {
principal: String,
cal_id: String,
id: String,
deleted: bool,
}
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let rows = sqlx::query_as!(
Row,
r#"
SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
FROM calendarobjects
WHERE (principal, cal_id, id) NOT IN (
SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog
)
;
"#,
)
.fetch_all(&mut *tx)
.await
.map_err(crate::Error::from)?;
for row in rows {
let operation = if row.deleted {
ChangeOperation::Delete
} else {
ChangeOperation::Add
};
warn!(
"Commiting orphaned calendar object ({},{},{}), deleted={}",
&row.principal, &row.cal_id, &row.id, &row.deleted
);
log_object_operation(&mut tx, &row.principal, &row.cal_id, &row.id, operation).await?;
}
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>( async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: &str, principal: &str,
@@ -351,9 +398,9 @@ impl SqliteCalendarStore {
#[instrument] #[instrument]
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>( async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: String, principal: &str,
cal_id: String, cal_id: &str,
object: CalendarObject, object: &CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics()); let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
@@ -600,18 +647,35 @@ impl CalendarStore for SqliteCalendarStore {
Self::_insert_calendar(&mut *tx, calendar.clone()).await?; Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
} }
let mut sync_token = None;
for object in objects { for object in objects {
Self::_put_object( Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
&mut *tx,
calendar.principal.clone(), sync_token = Some(
calendar.id.clone(), log_object_operation(
object, &mut tx,
false, &calendar.principal,
&calendar.id,
object.get_id(),
ChangeOperation::Add,
) )
.await?; .await?,
);
} }
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Some(sync_token) = sync_token
&& let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self
.get_calendar(&calendar.principal, &calendar.id, true)
.await?
.push_topic,
})
{
error!("Push notification about imported calendar failed: {err}");
}
Ok(()) Ok(())
} }
@@ -689,14 +753,7 @@ impl CalendarStore for SqliteCalendarStore {
return Err(Error::ReadOnly); return Err(Error::ReadOnly);
} }
Self::_put_object( Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
&mut *tx,
principal.clone(),
cal_id.clone(),
object,
overwrite,
)
.await?;
let sync_token = log_object_operation( let sync_token = log_object_operation(
&mut tx, &mut tx,
@@ -774,7 +831,7 @@ impl CalendarStore for SqliteCalendarStore {
data: CollectionOperationInfo::Content { sync_token }, data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id, true).await?.push_topic, topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about restored calendar object failed: {err}");
} }
Ok(()) Ok(())
} }
@@ -795,6 +852,7 @@ impl CalendarStore for SqliteCalendarStore {
} }
// Logs an operation to the events // Logs an operation to the events
// TODO: Log multiple updates
async fn log_object_operation( async fn log_object_operation(
tx: &mut Transaction<'_, Sqlite>, tx: &mut Transaction<'_, Sqlite>,
principal: &str, principal: &str,

View File

@@ -69,7 +69,9 @@ async fn get_data_stores(
let (send, recv) = tokio::sync::mpsc::channel(1000); let (send, recv) = tokio::sync::mpsc::channel(1000);
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone())); let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
addressbook_store.repair_orphans().await?;
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send)); let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
cal_store.repair_orphans().await?;
let subscription_store = Arc::new(SqliteStore::new(db.clone())); let subscription_store = Arc::new(SqliteStore::new(db.clone()));
let principal_store = Arc::new(SqlitePrincipalStore::new(db)); let principal_store = Arc::new(SqlitePrincipalStore::new(db));
( (