From 5878b93d62de35ca9caa8229737d7b6138c8011b Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 2 Nov 2025 20:45:31 +0100
Subject: [PATCH 01/17] add birthday_calendar table migrations
---
.../20251102192200_birthday_calendars.down.sql | 1 +
.../20251102192200_birthday_calendars.up.sql | 15 +++++++++++++++
2 files changed, 16 insertions(+)
create mode 100644 crates/store_sqlite/migrations/20251102192200_birthday_calendars.down.sql
create mode 100644 crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql
diff --git a/crates/store_sqlite/migrations/20251102192200_birthday_calendars.down.sql b/crates/store_sqlite/migrations/20251102192200_birthday_calendars.down.sql
new file mode 100644
index 0000000..d771fdb
--- /dev/null
+++ b/crates/store_sqlite/migrations/20251102192200_birthday_calendars.down.sql
@@ -0,0 +1 @@
+DROP TABLE birthday_calendars;
diff --git a/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql b/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql
new file mode 100644
index 0000000..0b83319
--- /dev/null
+++ b/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql
@@ -0,0 +1,15 @@
+CREATE TABLE birthday_calendars (
+ principal TEXT NOT NULL,
+ id TEXT NOT NULL,
+ displayname TEXT,
+ description TEXT,
+ "order" INT DEFAULT 0 NOT NULL,
+ color TEXT,
+ timezone_id TEXT,
+ deleted_at DATETIME,
+ push_topic TEXT NOT NULL,
+ PRIMARY KEY (principal, id),
+ CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
+ REFERENCES addressbooks (principal, id) ON DELETE CASCADE
+ -- birthday calendar stores no meaningful data so we can cascade
+)
From c19c3492c3573093c7f539420062f1405c708824 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 2 Nov 2025 20:45:58 +0100
Subject: [PATCH 02/17] frontend: Remove birthday calendar guard
---
.../public/templates/components/sections/calendars_section.html | 2 --
1 file changed, 2 deletions(-)
diff --git a/crates/frontend/public/templates/components/sections/calendars_section.html b/crates/frontend/public/templates/components/sections/calendars_section.html
index afbbee6..064e2fb 100644
--- a/crates/frontend/public/templates/components/sections/calendars_section.html
+++ b/crates/frontend/public/templates/components/sections/calendars_section.html
@@ -24,7 +24,6 @@
- {% if !calendar.id.starts_with("_birthdays_") %}
- {% endif %}
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
From 547e477ecaa8cf1a419a677b5e1669b28f16b0f6 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 2 Nov 2025 21:05:31 +0100
Subject: [PATCH 03/17] make sure a birthday calendar will be created for each
addressbook
---
.../20251102192200_birthday_calendars.up.sql | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql b/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql
index 0b83319..34878d0 100644
--- a/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql
+++ b/crates/store_sqlite/migrations/20251102192200_birthday_calendars.up.sql
@@ -12,4 +12,15 @@ CREATE TABLE birthday_calendars (
CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
REFERENCES addressbooks (principal, id) ON DELETE CASCADE
-- birthday calendar stores no meaningful data so we can cascade
-)
+);
+
+INSERT INTO birthday_calendars
+(principal, id, displayname, deleted_at, push_topic)
+SELECT
+ principal,
+ id,
+ displayname || ' birthdays' AS displayname,
+ deleted_at,
+ push_topic || substr(printf('%d', random()), -4) AS push_topic
+ -- jank suffix to ensure that new push_topic is different :D
+FROM addressbooks;
From 5cdbb3b9d3c3d259f9227065ced5204f8ecd6318 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 2 Nov 2025 21:06:43 +0100
Subject: [PATCH 04/17] migrate birthday store to sqlite
---
Cargo.lock | 2 +-
crates/store/Cargo.toml | 1 -
crates/store/src/contact_birthday_store.rs | 209 -----------
crates/store/src/lib.rs | 4 +-
crates/store_sqlite/Cargo.toml | 1 +
.../addressbook_store/birthday_calendar.rs | 353 ++++++++++++++++++
.../mod.rs} | 37 +-
src/app.rs | 11 +-
src/main.rs | 6 +-
9 files changed, 388 insertions(+), 236 deletions(-)
delete mode 100644 crates/store/src/contact_birthday_store.rs
create mode 100644 crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
rename crates/store_sqlite/src/{addressbook_store.rs => addressbook_store/mod.rs} (96%)
diff --git a/Cargo.lock b/Cargo.lock
index ba16073..84cdb25 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3227,7 +3227,6 @@ dependencies = [
"rustical_store_sqlite",
"rustical_xml",
"serde",
- "sha2",
"thiserror 2.0.17",
"tokio",
"tower",
@@ -3250,6 +3249,7 @@ dependencies = [
"rustical_ical",
"rustical_store",
"serde",
+ "sha2",
"sqlx",
"thiserror 2.0.17",
"tokio",
diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml
index 98823af..2426311 100644
--- a/crates/store/Cargo.toml
+++ b/crates/store/Cargo.toml
@@ -11,7 +11,6 @@ publish = false
anyhow = { workspace = true }
async-trait = { workspace = true }
serde = { workspace = true }
-sha2 = { workspace = true }
ical = { workspace = true }
chrono = { workspace = true }
regex = { workspace = true }
diff --git a/crates/store/src/contact_birthday_store.rs b/crates/store/src/contact_birthday_store.rs
deleted file mode 100644
index 507f328..0000000
--- a/crates/store/src/contact_birthday_store.rs
+++ /dev/null
@@ -1,209 +0,0 @@
-use crate::{
- Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
- combined_calendar_store::PrefixedCalendarStore,
-};
-use async_trait::async_trait;
-use derive_more::derive::Constructor;
-use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
-use sha2::{Digest, Sha256};
-use std::{collections::HashMap, sync::Arc};
-
-pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
-
-#[derive(Constructor, Clone)]
-pub struct ContactBirthdayStore
(Arc);
-
-impl PrefixedCalendarStore for ContactBirthdayStore {
- const PREFIX: &'static str = BIRTHDAYS_PREFIX;
-}
-
-fn birthday_calendar(addressbook: Addressbook) -> Calendar {
- Calendar {
- principal: addressbook.principal,
- id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
- meta: CalendarMetadata {
- displayname: addressbook
- .displayname
- .map(|name| format!("{name} birthdays")),
- order: 0,
- description: None,
- color: None,
- },
- timezone_id: None,
- deleted_at: addressbook.deleted_at,
- synctoken: addressbook.synctoken,
- subscription_url: None,
- push_topic: {
- let mut hasher = Sha256::new();
- hasher.update("birthdays");
- hasher.update(addressbook.push_topic);
- format!("{:x}", hasher.finalize())
- },
- components: vec![CalendarObjectType::Event],
- }
-}
-
-/// Objects are all prefixed with `BIRTHDAYS_PREFIX`
-#[async_trait]
-impl CalendarStore for ContactBirthdayStore {
- async fn get_calendar(
- &self,
- principal: &str,
- id: &str,
- show_deleted: bool,
- ) -> Result {
- let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
- let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
- Ok(birthday_calendar(addressbook))
- }
-
- async fn get_calendars(&self, principal: &str) -> Result, Error> {
- let addressbooks = self.0.get_addressbooks(principal).await?;
- Ok(addressbooks.into_iter().map(birthday_calendar).collect())
- }
-
- async fn get_deleted_calendars(&self, principal: &str) -> Result, Error> {
- let addressbooks = self.0.get_deleted_addressbooks(principal).await?;
- Ok(addressbooks.into_iter().map(birthday_calendar).collect())
- }
-
- async fn update_calendar(
- &self,
- _principal: String,
- _id: String,
- _calendar: Calendar,
- ) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
- async fn delete_calendar(
- &self,
- _principal: &str,
- _name: &str,
- _use_trashbin: bool,
- ) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- async fn import_calendar(
- &self,
- _calendar: Calendar,
- _objects: Vec,
- _merge_existing: bool,
- ) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- async fn sync_changes(
- &self,
- principal: &str,
- cal_id: &str,
- synctoken: i64,
- ) -> Result<(Vec, Vec, i64), Error> {
- let cal_id = cal_id
- .strip_prefix(BIRTHDAYS_PREFIX)
- .ok_or(Error::NotFound)?;
- let (objects, deleted_objects, new_synctoken) =
- self.0.sync_changes(principal, cal_id, synctoken).await?;
- let objects: Result>, rustical_ical::Error> = objects
- .iter()
- .map(AddressObject::get_birthday_object)
- .collect();
- let objects = objects?.into_iter().flatten().collect();
-
- Ok((objects, deleted_objects, new_synctoken))
- }
-
- async fn calendar_metadata(
- &self,
- principal: &str,
- cal_id: &str,
- ) -> Result {
- let cal_id = cal_id
- .strip_prefix(BIRTHDAYS_PREFIX)
- .ok_or(Error::NotFound)?;
- self.0.addressbook_metadata(principal, cal_id).await
- }
-
- async fn get_objects(
- &self,
- principal: &str,
- cal_id: &str,
- ) -> Result, Error> {
- let cal_id = cal_id
- .strip_prefix(BIRTHDAYS_PREFIX)
- .ok_or(Error::NotFound)?;
- let objects: Result>, rustical_ical::Error> =
- self.0
- .get_objects(principal, cal_id)
- .await?
- .iter()
- .map(AddressObject::get_significant_dates)
- .collect();
- let objects = objects?
- .into_iter()
- .flat_map(HashMap::into_values)
- .collect();
-
- Ok(objects)
- }
-
- async fn get_object(
- &self,
- principal: &str,
- cal_id: &str,
- object_id: &str,
- show_deleted: bool,
- ) -> Result {
- let cal_id = cal_id
- .strip_prefix(BIRTHDAYS_PREFIX)
- .ok_or(Error::NotFound)?;
- let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
- self.0
- .get_object(principal, cal_id, addressobject_id, show_deleted)
- .await?
- .get_significant_dates()?
- .remove(date_type)
- .ok_or(Error::NotFound)
- }
-
- async fn put_object(
- &self,
- _principal: String,
- _cal_id: String,
- _object: CalendarObject,
- _overwrite: bool,
- ) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- async fn delete_object(
- &self,
- _principal: &str,
- _cal_id: &str,
- _object_id: &str,
- _use_trashbin: bool,
- ) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- async fn restore_object(
- &self,
- _principal: &str,
- _cal_id: &str,
- _object_id: &str,
- ) -> Result<(), Error> {
- Err(Error::ReadOnly)
- }
-
- fn is_read_only(&self, _cal_id: &str) -> bool {
- true
- }
-}
diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs
index 73ac4d2..2dd9050 100644
--- a/crates/store/src/lib.rs
+++ b/crates/store/src/lib.rs
@@ -7,7 +7,6 @@ pub use error::Error;
pub mod auth;
mod calendar;
mod combined_calendar_store;
-mod contact_birthday_store;
mod secret;
mod subscription_store;
pub mod synctoken;
@@ -17,8 +16,7 @@ pub mod tests;
pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore;
-pub use combined_calendar_store::CombinedCalendarStore;
-pub use contact_birthday_store::ContactBirthdayStore;
+pub use combined_calendar_store::{CombinedCalendarStore, PrefixedCalendarStore};
pub use secret::Secret;
pub use subscription_store::*;
diff --git a/crates/store_sqlite/Cargo.toml b/crates/store_sqlite/Cargo.toml
index e4b67f4..084c4cf 100644
--- a/crates/store_sqlite/Cargo.toml
+++ b/crates/store_sqlite/Cargo.toml
@@ -29,3 +29,4 @@ uuid.workspace = true
pbkdf2.workspace = true
rustical_ical.workspace = true
rstest = { workspace = true, optional = true }
+sha2.workspace = true
diff --git a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
new file mode 100644
index 0000000..71ef5a6
--- /dev/null
+++ b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
@@ -0,0 +1,353 @@
+use crate::addressbook_store::SqliteAddressbookStore;
+use async_trait::async_trait;
+use chrono::NaiveDateTime;
+use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
+use rustical_store::{
+ Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
+ Error, PrefixedCalendarStore,
+};
+use sha2::{Digest, Sha256};
+use sqlx::{Executor, Sqlite};
+use std::collections::HashMap;
+use tracing::instrument;
+
+pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
+
+struct BirthdayCalendarJoinRow {
+ principal: String,
+ id: String,
+ displayname: Option,
+ description: Option,
+ order: i64,
+ color: Option,
+ timezone_id: Option,
+ deleted_at: Option,
+ push_topic: String,
+
+ addr_synctoken: i64,
+}
+
+impl From for Calendar {
+ fn from(value: BirthdayCalendarJoinRow) -> Self {
+ Self {
+ principal: value.principal,
+ id: format!("{}{}", BIRTHDAYS_PREFIX, value.id),
+ meta: CalendarMetadata {
+ displayname: value.displayname,
+ order: value.order,
+ description: value.description,
+ color: value.color,
+ },
+ deleted_at: value.deleted_at,
+ components: vec![CalendarObjectType::Event],
+ timezone_id: value.timezone_id,
+ synctoken: value.addr_synctoken,
+ subscription_url: None,
+ push_topic: value.push_topic,
+ }
+ }
+}
+
+impl PrefixedCalendarStore for SqliteAddressbookStore {
+ const PREFIX: &'static str = BIRTHDAYS_PREFIX;
+}
+
+impl SqliteAddressbookStore {
+ #[instrument]
+ pub async fn _get_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ id: &str,
+ show_deleted: bool,
+ ) -> Result {
+ let cal = sqlx::query_as!(
+ BirthdayCalendarJoinRow,
+ r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
+ FROM birthday_calendars
+ INNER JOIN (
+ SELECT principal AS addr_principal,
+ id AS addr_id,
+ synctoken AS addr_synctoken
+ FROM addressbooks
+ ) ON (principal, id) = (addr_principal, addr_id)
+ WHERE (principal, id) = (?, ?)
+ AND ((deleted_at IS NULL) OR ?)
+ "#,
+ principal,
+ id,
+ show_deleted
+ )
+ .fetch_one(executor)
+ .await
+ .map_err(crate::Error::from)?;
+ Ok(cal.into())
+ }
+
+ #[instrument]
+ pub async fn _get_birthday_calendars<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ deleted: bool,
+ ) -> Result, Error> {
+ Ok(
+ sqlx::query_as!(
+ BirthdayCalendarJoinRow,
+ r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
+ FROM birthday_calendars
+ INNER JOIN (
+ SELECT principal AS addr_principal,
+ id AS addr_id,
+ synctoken AS addr_synctoken
+ FROM addressbooks
+ ) ON (principal, id) = (addr_principal, addr_id)
+ WHERE principal = ?
+ AND (
+ (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted
+ OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted
+ )
+ "#,
+ principal,
+ deleted,
+ deleted
+ )
+ .fetch_all(executor)
+ .await
+ .map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
+ }
+
+ #[instrument]
+ pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ addressbook: Addressbook,
+ ) -> Result<(), rustical_store::Error> {
+ let birthday_name = addressbook
+ .displayname
+ .map(|name| format!("{name} birthdays"));
+ let birthday_push_topic = {
+ let mut hasher = Sha256::new();
+ hasher.update("birthdays");
+ hasher.update(addressbook.push_topic);
+ format!("{:x}", hasher.finalize())
+ };
+
+ sqlx::query!(
+ r#"INSERT INTO birthday_calendars (principal, id, displayname, push_topic)
+ VALUES (?, ?, ?, ?)"#,
+ addressbook.principal,
+ addressbook.id,
+ birthday_name,
+ birthday_push_topic,
+ )
+ .execute(executor)
+ .await
+ .map_err(crate::Error::from)?;
+ Ok(())
+ }
+
+ #[instrument]
+ async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ calendar: &Calendar,
+ ) -> Result<(), Error> {
+ let result = sqlx::query!(
+ r#"UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?
+ WHERE (principal, id) = (?, ?)"#,
+ calendar.principal,
+ calendar.id,
+ calendar.meta.displayname,
+ calendar.meta.description,
+ calendar.meta.order,
+ calendar.meta.color,
+ calendar.timezone_id,
+ calendar.push_topic,
+ principal,
+ calendar.id,
+ ).execute(executor).await.map_err(crate::Error::from)?;
+ if result.rows_affected() == 0 {
+ return Err(rustical_store::Error::NotFound);
+ }
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl CalendarStore for SqliteAddressbookStore {
+ #[instrument]
+ async fn get_calendar(
+ &self,
+ principal: &str,
+ id: &str,
+ show_deleted: bool,
+ ) -> Result {
+ let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
+ Self::_get_birthday_calendar(&self.db, principal, id, show_deleted).await
+ }
+
+ #[instrument]
+ async fn get_calendars(&self, principal: &str) -> Result, Error> {
+ Self::_get_birthday_calendars(&self.db, principal, false).await
+ }
+
+ #[instrument]
+ async fn get_deleted_calendars(&self, principal: &str) -> Result, Error> {
+ Self::_get_birthday_calendars(&self.db, principal, true).await
+ }
+
+ #[instrument]
+ async fn update_calendar(
+ &self,
+ principal: String,
+ id: String,
+ mut calendar: Calendar,
+ ) -> Result<(), Error> {
+ assert_eq!(id, calendar.id);
+ calendar.id = calendar
+ .id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?
+ .to_string();
+ Self::_update_birthday_calendar(&self.db, &principal, &calendar).await
+ }
+
+ #[instrument]
+ async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ #[instrument]
+ async fn delete_calendar(
+ &self,
+ _principal: &str,
+ _name: &str,
+ _use_trashbin: bool,
+ ) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ #[instrument]
+ async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ #[instrument]
+ async fn import_calendar(
+ &self,
+ _calendar: Calendar,
+ _objects: Vec,
+ _merge_existing: bool,
+ ) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ #[instrument]
+ async fn sync_changes(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ synctoken: i64,
+ ) -> Result<(Vec, Vec, i64), Error> {
+ let cal_id = cal_id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?;
+ let (objects, deleted_objects, new_synctoken) =
+ AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
+ let objects: Result>, rustical_ical::Error> = objects
+ .iter()
+ .map(AddressObject::get_birthday_object)
+ .collect();
+ let objects = objects?.into_iter().flatten().collect();
+
+ Ok((objects, deleted_objects, new_synctoken))
+ }
+
+ #[instrument]
+ async fn calendar_metadata(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result {
+ let cal_id = cal_id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?;
+ self.addressbook_metadata(principal, cal_id).await
+ }
+
+ #[instrument]
+ async fn get_objects(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ ) -> Result, Error> {
+ let cal_id = cal_id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?;
+ let objects: Result>, rustical_ical::Error> =
+ AddressbookStore::get_objects(self, principal, cal_id)
+ .await?
+ .iter()
+ .map(AddressObject::get_significant_dates)
+ .collect();
+ let objects = objects?
+ .into_iter()
+ .flat_map(HashMap::into_values)
+ .collect();
+
+ Ok(objects)
+ }
+
+ #[instrument]
+ async fn get_object(
+ &self,
+ principal: &str,
+ cal_id: &str,
+ object_id: &str,
+ show_deleted: bool,
+ ) -> Result {
+ let cal_id = cal_id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?;
+ let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
+ AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
+ .await?
+ .get_significant_dates()?
+ .remove(date_type)
+ .ok_or(Error::NotFound)
+ }
+
+ #[instrument]
+ async fn put_object(
+ &self,
+ _principal: String,
+ _cal_id: String,
+ _object: CalendarObject,
+ _overwrite: bool,
+ ) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ #[instrument]
+ async fn delete_object(
+ &self,
+ _principal: &str,
+ _cal_id: &str,
+ _object_id: &str,
+ _use_trashbin: bool,
+ ) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ #[instrument]
+ async fn restore_object(
+ &self,
+ _principal: &str,
+ _cal_id: &str,
+ _object_id: &str,
+ ) -> Result<(), Error> {
+ Err(Error::ReadOnly)
+ }
+
+ fn is_read_only(&self, _cal_id: &str) -> bool {
+ true
+ }
+}
diff --git a/crates/store_sqlite/src/addressbook_store.rs b/crates/store_sqlite/src/addressbook_store/mod.rs
similarity index 96%
rename from crates/store_sqlite/src/addressbook_store.rs
rename to crates/store_sqlite/src/addressbook_store/mod.rs
index 497bfdb..e4794dd 100644
--- a/crates/store_sqlite/src/addressbook_store.rs
+++ b/crates/store_sqlite/src/addressbook_store/mod.rs
@@ -11,6 +11,8 @@ use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
use tracing::{error, instrument};
+pub mod birthday_calendar;
+
#[derive(Debug, Clone)]
struct AddressObjectRow {
id: String,
@@ -116,7 +118,7 @@ impl SqliteAddressbookStore {
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
- addressbook: Addressbook,
+ addressbook: &Addressbook,
) -> Result<(), rustical_store::Error> {
sqlx::query!(
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
@@ -283,9 +285,9 @@ impl SqliteAddressbookStore {
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
- principal: String,
- addressbook_id: String,
- object: AddressObject,
+ principal: &str,
+ addressbook_id: &str,
+ object: &AddressObject,
overwrite: bool,
) -> Result<(), rustical_store::Error> {
let (object_id, vcf) = (object.get_id(), object.get_vcf());
@@ -405,7 +407,15 @@ impl AddressbookStore for SqliteAddressbookStore {
&self,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
- Self::_insert_addressbook(&self.db, addressbook).await
+ let mut tx = self
+ .db
+ .begin_with(BEGIN_IMMEDIATE)
+ .await
+ .map_err(crate::Error::from)?;
+ Self::_insert_addressbook(&mut *tx, &addressbook).await?;
+ Self::_insert_birthday_calendar(&mut *tx, addressbook).await?;
+ tx.commit().await.map_err(crate::Error::from)?;
+ Ok(())
}
#[instrument]
@@ -521,14 +531,7 @@ impl AddressbookStore for SqliteAddressbookStore {
let object_id = object.get_id().to_owned();
- Self::_put_object(
- &mut *tx,
- principal.clone(),
- addressbook_id.clone(),
- object,
- overwrite,
- )
- .await?;
+ Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?;
let sync_token = log_object_operation(
&mut tx,
@@ -659,15 +662,15 @@ impl AddressbookStore for SqliteAddressbookStore {
return Err(Error::AlreadyExists);
}
if existing.is_none() {
- Self::_insert_addressbook(&mut *tx, addressbook.clone()).await?;
+ Self::_insert_addressbook(&mut *tx, &addressbook).await?;
}
for object in objects {
Self::_put_object(
&mut *tx,
- addressbook.principal.clone(),
- addressbook.id.clone(),
- object,
+ &addressbook.principal,
+ &addressbook.id,
+ &object,
false,
)
.await?;
diff --git a/src/app.rs b/src/app.rs
index 7634d2c..2a16b91 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -16,7 +16,8 @@ use rustical_frontend::{FrontendConfig, frontend_router};
use rustical_oidc::OidcConfig;
use rustical_store::auth::AuthenticationProvider;
use rustical_store::{
- AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore,
+ AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
+ SubscriptionStore,
};
use std::sync::Arc;
use std::time::Duration;
@@ -33,7 +34,11 @@ use tracing::field::display;
clippy::too_many_lines,
clippy::cognitive_complexity
)]
-pub fn make_app(
+pub fn make_app<
+ AS: AddressbookStore + PrefixedCalendarStore,
+ CS: CalendarStore,
+ S: SubscriptionStore,
+>(
addr_store: Arc,
cal_store: Arc,
subscription_store: Arc,
@@ -45,7 +50,7 @@ pub fn make_app(
session_cookie_samesite_strict: bool,
payload_limit_mb: usize,
) -> Router<()> {
- let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
+ let birthday_store = addr_store.clone();
let combined_cal_store =
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));
diff --git a/src/main.rs b/src/main.rs
index b4d88f3..706fa20 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,7 +13,9 @@ use figment::Figment;
use figment::providers::{Env, Format, Toml};
use rustical_dav_push::DavPushController;
use rustical_store::auth::AuthenticationProvider;
-use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
+use rustical_store::{
+ AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore,
+};
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
@@ -56,7 +58,7 @@ async fn get_data_stores(
migrate: bool,
config: &DataStoreConfig,
) -> Result<(
- Arc,
+ Arc,
Arc,
Arc,
Arc,
From 425d10cb991b9414c55c5b1eaf8046c7b435a902 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 2 Nov 2025 21:07:06 +0100
Subject: [PATCH 05/17] CalendarStore::is_read_only now refers to its content
only and not its metadata
---
crates/caldav/src/calendar/resource.rs | 7 +------
crates/store/src/calendar_store.rs | 1 +
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs
index 2c891eb..03bbe04 100644
--- a/crates/caldav/src/calendar/resource.rs
+++ b/crates/caldav/src/calendar/resource.rs
@@ -317,16 +317,11 @@ impl Resource for CalendarResource {
}
fn get_user_privileges(&self, user: &Principal) -> Result {
- if self.cal.subscription_url.is_some() {
+ if self.cal.subscription_url.is_some() || self.read_only {
return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal),
));
}
- if self.read_only {
- return Ok(UserPrivilegeSet::owner_read(
- user.is_principal(&self.cal.principal),
- ));
- }
Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.cal.principal),
diff --git a/crates/store/src/calendar_store.rs b/crates/store/src/calendar_store.rs
index 94dd83a..99b9c4e 100644
--- a/crates/store/src/calendar_store.rs
+++ b/crates/store/src/calendar_store.rs
@@ -98,5 +98,6 @@ pub trait CalendarStore: Send + Sync + 'static {
object_id: &str,
) -> Result<(), Error>;
+ // read_only refers to objects, metadata may still be updated
fn is_read_only(&self, cal_id: &str) -> bool;
}
From 381af1b877923a2d1577adc2e01811df84db2b9b Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Mon, 3 Nov 2025 15:37:40 +0100
Subject: [PATCH 06/17] run .sqlx prepare
---
...1cc16f43768074ab9e7ab7b7783395384984e.json | 12 +++
...809943f817abf8c8f9ae50073924bccdea2dc.json | 74 +++++++++++++++++++
...110e1efbc7ce9332f10da4fa69f7594fb7455.json | 74 +++++++++++++++++++
...1d32ac367c52ce41bd70394f754248b29749c.json | 12 +++
4 files changed, 172 insertions(+)
create mode 100644 .sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
create mode 100644 .sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
create mode 100644 .sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
create mode 100644 .sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json
diff --git a/.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json b/.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
new file mode 100644
index 0000000..9298694
--- /dev/null
+++ b/.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 10
+ },
+ "nullable": []
+ },
+ "hash": "4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e"
+}
diff --git a/.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json b/.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
new file mode 100644
index 0000000..794b59e
--- /dev/null
+++ b/.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
@@ -0,0 +1,74 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?)\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "principal",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "displayname",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "description",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "order",
+ "ordinal": 4,
+ "type_info": "Integer"
+ },
+ {
+ "name": "color",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "timezone_id",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_at",
+ "ordinal": 7,
+ "type_info": "Datetime"
+ },
+ {
+ "name": "addr_synctoken",
+ "ordinal": 8,
+ "type_info": "Integer"
+ },
+ {
+ "name": "push_topic",
+ "ordinal": 9,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ false,
+ false,
+ true,
+ true,
+ false,
+ true,
+ true,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc"
+}
diff --git a/.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json b/.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
new file mode 100644
index 0000000..e738181
--- /dev/null
+++ b/.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
@@ -0,0 +1,74 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE principal = ?\n AND (\n (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted\n OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted\n )\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "principal",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "displayname",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "description",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "order",
+ "ordinal": 4,
+ "type_info": "Integer"
+ },
+ {
+ "name": "color",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "timezone_id",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "deleted_at",
+ "ordinal": 7,
+ "type_info": "Datetime"
+ },
+ {
+ "name": "addr_synctoken",
+ "ordinal": 8,
+ "type_info": "Integer"
+ },
+ {
+ "name": "push_topic",
+ "ordinal": 9,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ false,
+ false,
+ true,
+ true,
+ false,
+ true,
+ true,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455"
+}
diff --git a/.sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json b/.sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json
new file mode 100644
index 0000000..3d85443
--- /dev/null
+++ b/.sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "INSERT INTO birthday_calendars (principal, id, displayname, push_topic)\n VALUES (?, ?, ?, ?)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 4
+ },
+ "nullable": []
+ },
+ "hash": "bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c"
+}
From 7bf00da0e58375999b67f87a6e46782f8c23b5b6 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Tue, 4 Nov 2025 16:56:17 +0100
Subject: [PATCH 07/17] implement deleting and restoring birthday calendars
---
.../addressbook_store/birthday_calendar.rs | 62 +++++++++++++++++--
1 file changed, 56 insertions(+), 6 deletions(-)
diff --git a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
index 71ef5a6..3c310c8 100644
--- a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
+++ b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
@@ -144,6 +144,50 @@ impl SqliteAddressbookStore {
Ok(())
}
+ async fn _delete_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ id: &str,
+ use_trashbin: bool,
+ ) -> Result<(), Error> {
+ if use_trashbin {
+ sqlx::query!(
+ r#"UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
+ principal,
+ id
+ )
+ .execute(executor)
+ .await
+ .map_err(crate::Error::from)?
+ } else {
+ sqlx::query!(
+ r#"DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)"#,
+ principal,
+ id
+ )
+ .execute(executor)
+ .await
+ .map_err(crate::Error::from)?
+ };
+ Ok(())
+ }
+
+ async fn _restore_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ principal: &str,
+ id: &str,
+ ) -> Result<(), Error> {
+ sqlx::query!(
+ r"UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
+ principal,
+ id
+ )
+ .execute(executor)
+ .await
+ .map_err(crate::Error::from)?;
+ Ok(())
+ }
+
#[instrument]
async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
@@ -218,16 +262,22 @@ impl CalendarStore for SqliteAddressbookStore {
#[instrument]
async fn delete_calendar(
&self,
- _principal: &str,
- _name: &str,
- _use_trashbin: bool,
+ principal: &str,
+ id: &str,
+ use_trashbin: bool,
) -> Result<(), Error> {
- Err(Error::ReadOnly)
+ let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
+ return Ok(());
+ };
+ Self::_delete_birthday_calendar(&self.db, principal, id, use_trashbin).await
}
#[instrument]
- async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
- Err(Error::ReadOnly)
+ async fn restore_calendar(&self, principal: &str, id: &str) -> Result<(), Error> {
+ let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
+ return Err(Error::NotFound);
+ };
+ Self::_restore_birthday_calendar(&self.db, principal, id).await
}
#[instrument]
From 5588137f73e685a80b09f5fd25853ff44b7a60c9 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Tue, 4 Nov 2025 17:01:54 +0100
Subject: [PATCH 08/17] sqlx prepare
---
...492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json | 12 ++++++++++++
...2019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json | 12 ++++++++++++
...1ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json | 12 ++++++++++++
3 files changed, 36 insertions(+)
create mode 100644 .sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
create mode 100644 .sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
create mode 100644 .sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
diff --git a/.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json b/.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
new file mode 100644
index 0000000..25390d9
--- /dev/null
+++ b/.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896"
+}
diff --git a/.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json b/.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
new file mode 100644
index 0000000..e08aeb3
--- /dev/null
+++ b/.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc"
+}
diff --git a/.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json b/.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
new file mode 100644
index 0000000..442774a
--- /dev/null
+++ b/.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a"
+}
From 873b40ad10a783b9d982059dc01bf47d35c3d112 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:05:55 +0100
Subject: [PATCH 09/17] stylesheet: Add flex-wrap to actions
---
crates/frontend/public/assets/style.css | 1 +
1 file changed, 1 insertion(+)
diff --git a/crates/frontend/public/assets/style.css b/crates/frontend/public/assets/style.css
index 2c1c2c1..6a21965 100644
--- a/crates/frontend/public/assets/style.css
+++ b/crates/frontend/public/assets/style.css
@@ -282,6 +282,7 @@ ul.collection-list {
grid-area: actions;
width: fit-content;
display: flex;
+ flex-wrap: wrap;
gap: 12px;
}
}
From 96f221f721e36b1e27d4d69ea59b62b5d84ccd61 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sat, 22 Nov 2025 18:32:53 +0100
Subject: [PATCH 10/17] birthday_calendar: Refactor insert_birthday_calendar
---
Cargo.lock | 1 +
.../store_sqlite/src/addressbook_store/birthday_calendar.rs | 5 +++--
crates/store_sqlite/src/addressbook_store/mod.rs | 2 +-
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2d3e5a0..b834b0a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3230,6 +3230,7 @@ dependencies = [
"rustical_store_sqlite",
"rustical_xml",
"serde",
+ "sha2",
"thiserror 2.0.17",
"tokio",
"tower",
diff --git a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
index 3c310c8..5043e14 100644
--- a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
+++ b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
@@ -118,15 +118,16 @@ impl SqliteAddressbookStore {
#[instrument]
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
- addressbook: Addressbook,
+ addressbook: &Addressbook,
) -> Result<(), rustical_store::Error> {
let birthday_name = addressbook
.displayname
+ .as_ref()
.map(|name| format!("{name} birthdays"));
let birthday_push_topic = {
let mut hasher = Sha256::new();
hasher.update("birthdays");
- hasher.update(addressbook.push_topic);
+ hasher.update(&addressbook.push_topic);
format!("{:x}", hasher.finalize())
};
diff --git a/crates/store_sqlite/src/addressbook_store/mod.rs b/crates/store_sqlite/src/addressbook_store/mod.rs
index 56a9c90..8d755d6 100644
--- a/crates/store_sqlite/src/addressbook_store/mod.rs
+++ b/crates/store_sqlite/src/addressbook_store/mod.rs
@@ -467,7 +467,7 @@ impl AddressbookStore for SqliteAddressbookStore {
.await
.map_err(crate::Error::from)?;
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
- Self::_insert_birthday_calendar(&mut *tx, addressbook).await?;
+ Self::_insert_birthday_calendar(&mut *tx, &addressbook).await?;
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
From a79e1901b805596de29319f03549a7d43bd4d993 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sat, 22 Nov 2025 18:48:36 +0100
Subject: [PATCH 11/17] test_propfind: Revert assert_eq order
---
crates/caldav/src/calendar/tests.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/caldav/src/calendar/tests.rs b/crates/caldav/src/calendar/tests.rs
index cebcdc0..cb6c8c5 100644
--- a/crates/caldav/src/calendar/tests.rs
+++ b/crates/caldav/src/calendar/tests.rs
@@ -39,7 +39,7 @@ async fn test_propfind() {
.unwrap()
.trim()
.replace("\r\n", "\n");
- similar_asserts::assert_eq!(output, expected_output);
+ similar_asserts::assert_eq!(expected_output, output);
}
}
}
From d5c1ddc590b6d7dea7b6660e3133b042fd506199 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sat, 22 Nov 2025 18:49:32 +0100
Subject: [PATCH 12/17] caldav: Update test_propfind regression test
---
crates/caldav/src/calendar/test_files/propfind.outputs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/crates/caldav/src/calendar/test_files/propfind.outputs b/crates/caldav/src/calendar/test_files/propfind.outputs
index 9330cdf..9a28b63 100644
--- a/crates/caldav/src/calendar/test_files/propfind.outputs
+++ b/crates/caldav/src/calendar/test_files/propfind.outputs
@@ -211,6 +211,9 @@ END:VCALENDAR
+
+
+
From e39657eb297b021ad88a81040c02c830d889b607 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Fri, 5 Dec 2025 14:48:11 +0100
Subject: [PATCH 13/17] PROPPATCH: Fix privileges
---
crates/dav/src/resource/methods/proppatch.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/dav/src/resource/methods/proppatch.rs b/crates/dav/src/resource/methods/proppatch.rs
index dc24821..cbeb8b0 100644
--- a/crates/dav/src/resource/methods/proppatch.rs
+++ b/crates/dav/src/resource/methods/proppatch.rs
@@ -88,7 +88,7 @@ pub async fn route_proppatch(
.get_resource(path_components, false)
.await?;
let privileges = resource.get_user_privileges(principal)?;
- if !privileges.has(&UserPrivilege::Write) {
+ if !privileges.has(&UserPrivilege::WriteProperties) {
return Err(Error::Unauthorized.into());
}
From e99b1d912353a9f580b7f1a0cb961e886e50f0bf Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Fri, 5 Dec 2025 14:48:35 +0100
Subject: [PATCH 14/17] calendar resource: Remove prop write guards
---
crates/caldav/src/calendar/resource.rs | 6 ------
1 file changed, 6 deletions(-)
diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs
index 03bbe04..65d5912 100644
--- a/crates/caldav/src/calendar/resource.rs
+++ b/crates/caldav/src/calendar/resource.rs
@@ -188,9 +188,6 @@ impl Resource for CalendarResource {
}
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
- if self.read_only {
- return Err(rustical_dav::Error::PropReadOnly);
- }
match prop {
CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => {
@@ -263,9 +260,6 @@ impl Resource for CalendarResource {
}
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
- if self.read_only {
- return Err(rustical_dav::Error::PropReadOnly);
- }
match prop {
CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => {
From af239e34bf474d8fde8a29190ca9469432385c31 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Fri, 5 Dec 2025 14:49:09 +0100
Subject: [PATCH 15/17] birthday calendar store: Support manual birthday
calendar creation
---
.../addressbook_store/birthday_calendar.rs | 53 ++++++++++++++-----
.../store_sqlite/src/addressbook_store/mod.rs | 3 +-
2 files changed, 42 insertions(+), 14 deletions(-)
diff --git a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
index 5043e14..4ca06a5 100644
--- a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
+++ b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
@@ -115,11 +115,8 @@ impl SqliteAddressbookStore {
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
}
- #[instrument]
- pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
- executor: E,
- addressbook: &Addressbook,
- ) -> Result<(), rustical_store::Error> {
+ #[must_use]
+ pub fn default_birthday_calendar(addressbook: Addressbook) -> Calendar {
let birthday_name = addressbook
.displayname
.as_ref()
@@ -130,14 +127,44 @@ impl SqliteAddressbookStore {
hasher.update(&addressbook.push_topic);
format!("{:x}", hasher.finalize())
};
+ Calendar {
+ principal: addressbook.principal,
+ meta: CalendarMetadata {
+ displayname: birthday_name,
+ order: 0,
+ description: None,
+ color: None,
+ },
+ id: format!("{}{}", Self::PREFIX, addressbook.id),
+ components: vec![CalendarObjectType::Event],
+ timezone_id: None,
+ deleted_at: None,
+ synctoken: Default::default(),
+ subscription_url: None,
+ push_topic: birthday_push_topic,
+ }
+ }
+
+ #[instrument]
+ pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
+ executor: E,
+ calendar: &Calendar,
+ ) -> Result<(), rustical_store::Error> {
+ let id = calendar
+ .id
+ .strip_prefix(BIRTHDAYS_PREFIX)
+ .ok_or(Error::NotFound)?;
sqlx::query!(
- r#"INSERT INTO birthday_calendars (principal, id, displayname, push_topic)
- VALUES (?, ?, ?, ?)"#,
- addressbook.principal,
- addressbook.id,
- birthday_name,
- birthday_push_topic,
+ r#"INSERT INTO birthday_calendars (principal, id, displayname, description, "order", color, push_topic)
+ VALUES (?, ?, ?, ?, ?, ?, ?)"#,
+ calendar.principal,
+ id,
+ calendar.meta.displayname,
+ calendar.meta.description,
+ calendar.meta.order,
+ calendar.meta.color,
+ calendar.push_topic,
)
.execute(executor)
.await
@@ -256,8 +283,8 @@ impl CalendarStore for SqliteAddressbookStore {
}
#[instrument]
- async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
- Err(Error::ReadOnly)
+ async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
+ Self::_insert_birthday_calendar(&self.db, &calendar).await
}
#[instrument]
diff --git a/crates/store_sqlite/src/addressbook_store/mod.rs b/crates/store_sqlite/src/addressbook_store/mod.rs
index 8d755d6..04e5730 100644
--- a/crates/store_sqlite/src/addressbook_store/mod.rs
+++ b/crates/store_sqlite/src/addressbook_store/mod.rs
@@ -467,7 +467,8 @@ impl AddressbookStore for SqliteAddressbookStore {
.await
.map_err(crate::Error::from)?;
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
- Self::_insert_birthday_calendar(&mut *tx, &addressbook).await?;
+ let birthday_cal = Self::default_birthday_calendar(addressbook);
+ Self::_insert_birthday_calendar(&mut *tx, &birthday_cal).await?;
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
From 57275a10b444ff273fbd8e1bdcb465a16f647fd3 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Fri, 5 Dec 2025 14:50:02 +0100
Subject: [PATCH 16/17] Add birthday calendar creation to frontend
---
.../lib/create-birthday-calendar-form.ts | 102 +++++++++++++++
crates/frontend/js-components/vite.config.ts | 1 +
.../js/create-birthday-calendar-form.mjs | 122 ++++++++++++++++++
.../sections/addressbooks_section.html | 12 +-
.../frontend/public/templates/pages/user.html | 1 +
crates/frontend/src/lib.rs | 8 +-
crates/frontend/src/routes/addressbooks.rs | 34 ++++-
7 files changed, 270 insertions(+), 10 deletions(-)
create mode 100644 crates/frontend/js-components/lib/create-birthday-calendar-form.ts
create mode 100644 crates/frontend/public/assets/js/create-birthday-calendar-form.mjs
diff --git a/crates/frontend/js-components/lib/create-birthday-calendar-form.ts b/crates/frontend/js-components/lib/create-birthday-calendar-form.ts
new file mode 100644
index 0000000..a99ec9a
--- /dev/null
+++ b/crates/frontend/js-components/lib/create-birthday-calendar-form.ts
@@ -0,0 +1,102 @@
+import { html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { Ref, createRef, ref } from 'lit/directives/ref.js';
+import { escapeXml } from ".";
+import { getTimezones } from "./timezones.ts";
+
+@customElement("create-birthday-calendar-form")
+export class CreateCalendarForm extends LitElement {
+ protected override createRenderRoot() {
+ return this
+ }
+
+ @property()
+ principal: string = ''
+ @property()
+ addr_id: string = ''
+ @property()
+ displayname: string = ''
+ @property()
+ description: string = ''
+ @property()
+ color: string = ''
+
+ dialog: Ref = createRef()
+ form: Ref = createRef()
+ @property()
+ timezones: Array = []
+
+ override render() {
+ return html`
+
+
+ `
+ }
+
+ async submit(e: SubmitEvent) {
+ e.preventDefault()
+ if (!this.addr_id) {
+ alert("Empty id")
+ return
+ }
+ if (!this.displayname) {
+ alert("Empty displayname")
+ return
+ }
+
+ let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
+ method: 'MKCOL',
+ headers: {
+ 'Content-Type': 'application/xml'
+ },
+ body: `
+
+
+
+ ${escapeXml(this.displayname)}
+ ${this.description ? `${escapeXml(this.description)}` : ''}
+ ${this.color ? `${escapeXml(this.color)}` : ''}
+
+
+
+
+
+
+ `
+ })
+
+ if (response.status >= 400) {
+ alert(`Error ${response.status}: ${await response.text()}`)
+ return null
+ }
+ window.location.reload()
+ return null
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'create-calendar-form': CreateCalendarForm
+ }
+}
diff --git a/crates/frontend/js-components/vite.config.ts b/crates/frontend/js-components/vite.config.ts
index 7480569..9397a06 100644
--- a/crates/frontend/js-components/vite.config.ts
+++ b/crates/frontend/js-components/vite.config.ts
@@ -14,6 +14,7 @@ export default defineConfig({
rollupOptions: {
input: [
+ "lib/create-birthday-calendar-form.ts",
"lib/create-calendar-form.ts",
"lib/edit-calendar-form.ts",
"lib/import-calendar-form.ts",
diff --git a/crates/frontend/public/assets/js/create-birthday-calendar-form.mjs b/crates/frontend/public/assets/js/create-birthday-calendar-form.mjs
new file mode 100644
index 0000000..7978fc8
--- /dev/null
+++ b/crates/frontend/public/assets/js/create-birthday-calendar-form.mjs
@@ -0,0 +1,122 @@
+import { i, x } from "./lit-DkXrt_Iv.mjs";
+import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
+import { e, n } from "./ref-BwbQvJBB.mjs";
+import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __decorateClass = (decorators, target, key, kind) => {
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
+ for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
+ if (decorator = decorators[i2])
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
+ if (kind && result) __defProp(target, key, result);
+ return result;
+};
+let CreateCalendarForm = class extends i {
+ constructor() {
+ super(...arguments);
+ this.principal = "";
+ this.addr_id = "";
+ this.displayname = "";
+ this.description = "";
+ this.color = "";
+ this.dialog = e();
+ this.form = e();
+ this.timezones = [];
+ }
+ createRenderRoot() {
+ return this;
+ }
+ render() {
+ return x`
+
+
+ `;
+ }
+ async submit(e2) {
+ e2.preventDefault();
+ if (!this.addr_id) {
+ alert("Empty id");
+ return;
+ }
+ if (!this.displayname) {
+ alert("Empty displayname");
+ return;
+ }
+ let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
+ method: "MKCOL",
+ headers: {
+ "Content-Type": "application/xml"
+ },
+ body: `
+
+
+
+ ${escapeXml(this.displayname)}
+ ${this.description ? `${escapeXml(this.description)}` : ""}
+ ${this.color ? `${escapeXml(this.color)}` : ""}
+
+
+
+
+
+
+ `
+ });
+ if (response.status >= 400) {
+ alert(`Error ${response.status}: ${await response.text()}`);
+ return null;
+ }
+ window.location.reload();
+ return null;
+ }
+};
+__decorateClass([
+ n$1()
+], CreateCalendarForm.prototype, "principal", 2);
+__decorateClass([
+ n$1()
+], CreateCalendarForm.prototype, "addr_id", 2);
+__decorateClass([
+ n$1()
+], CreateCalendarForm.prototype, "displayname", 2);
+__decorateClass([
+ n$1()
+], CreateCalendarForm.prototype, "description", 2);
+__decorateClass([
+ n$1()
+], CreateCalendarForm.prototype, "color", 2);
+__decorateClass([
+ n$1()
+], CreateCalendarForm.prototype, "timezones", 2);
+CreateCalendarForm = __decorateClass([
+ t("create-birthday-calendar-form")
+], CreateCalendarForm);
+export {
+ CreateCalendarForm
+};
diff --git a/crates/frontend/public/templates/components/sections/addressbooks_section.html b/crates/frontend/public/templates/components/sections/addressbooks_section.html
index e699b0b..cf2291c 100644
--- a/crates/frontend/public/templates/components/sections/addressbooks_section.html
+++ b/crates/frontend/public/templates/components/sections/addressbooks_section.html
@@ -1,6 +1,6 @@
{{user.id }}'s Addressbooks
- {% for (meta, addressbook) in addressbooks %}
+ {% for (meta, birthday_cal, addressbook) in addressbooks %}
-
@@ -24,9 +24,17 @@
>
+ {% if !birthday_cal.is_some() %}
+
+ {% endif %}
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
+ {{ birthday_cal.is_some() }}
@@ -37,7 +45,7 @@
{%if !deleted_addressbooks.is_empty() %}
Deleted Addressbooks
- {% for (meta, addressbook) in deleted_addressbooks %}
+ {% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
-
diff --git a/crates/frontend/public/templates/pages/user.html b/crates/frontend/public/templates/pages/user.html
index e4c2f88..ce658c2 100644
--- a/crates/frontend/public/templates/pages/user.html
+++ b/crates/frontend/public/templates/pages/user.html
@@ -5,6 +5,7 @@
+
diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs
index cee1d06..84f6751 100644
--- a/crates/frontend/src/lib.rs
+++ b/crates/frontend/src/lib.rs
@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
use rustical_store::{
- AddressbookStore, CalendarStore,
+ AddressbookStore, CalendarStore, PrefixedCalendarStore,
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
};
use std::sync::Arc;
@@ -39,7 +39,11 @@ use crate::routes::{
#[cfg(not(feature = "dev"))]
use assets::{Assets, EmbedService};
-pub fn frontend_router
(
+pub fn frontend_router<
+ AP: AuthenticationProvider,
+ CS: CalendarStore,
+ AS: AddressbookStore + PrefixedCalendarStore,
+>(
prefix: &'static str,
auth_provider: Arc,
cal_store: Arc,
diff --git a/crates/frontend/src/routes/addressbooks.rs b/crates/frontend/src/routes/addressbooks.rs
index f0707fc..4b446ef 100644
--- a/crates/frontend/src/routes/addressbooks.rs
+++ b/crates/frontend/src/routes/addressbooks.rs
@@ -1,10 +1,12 @@
-use std::sync::Arc;
-
use askama::Template;
use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode;
-use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
+use rustical_store::{
+ Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
+ auth::Principal,
+};
+use std::sync::Arc;
use crate::pages::user::{Section, UserPage};
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
#[template(path = "components/sections/addressbooks_section.html")]
pub struct AddressbooksSection {
pub user: Principal,
- pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
- pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
+ pub addressbooks: Vec<(CollectionMetadata, Option, Addressbook)>,
+ pub deleted_addressbooks: Vec<(CollectionMetadata, Option, Addressbook)>,
}
-pub async fn route_addressbooks(
+pub async fn route_addressbooks(
Path(user_id): Path,
Extension(addr_store): Extension>,
user: Principal,
@@ -43,22 +45,42 @@ pub async fn route_addressbooks(
let mut addressbook_infos = vec![];
for addressbook in addressbooks {
+ let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
+ let birthday_cal = match addr_store
+ .get_calendar(&addressbook.principal, &birthday_id, true)
+ .await
+ {
+ Ok(cal) => Some(cal),
+ Err(rustical_store::Error::NotFound) => None,
+ err => Some(err.unwrap()),
+ };
addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
+ birthday_cal,
addressbook,
));
}
let mut deleted_addressbook_infos = vec![];
for addressbook in deleted_addressbooks {
+ let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
+ let birthday_cal = match addr_store
+ .get_calendar(&addressbook.principal, &birthday_id, true)
+ .await
+ {
+ Ok(cal) => Some(cal),
+ Err(rustical_store::Error::NotFound) => None,
+ err => Some(err.unwrap()),
+ };
deleted_addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
+ birthday_cal,
addressbook,
));
}
From 96a16951f4be3ba054ff8f56a0ae2ea8975db80f Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Fri, 5 Dec 2025 14:55:30 +0100
Subject: [PATCH 17/17] sqlx prepare
---
...54d96bbcb717815081f74575f0a65987163faf9fe30a.json | 12 ++++++++++++
...f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json | 12 ------------
2 files changed, 12 insertions(+), 12 deletions(-)
create mode 100644 .sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
delete mode 100644 .sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json
diff --git a/.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json b/.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
new file mode 100644
index 0000000..52b6314
--- /dev/null
+++ b/.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "INSERT INTO birthday_calendars (principal, id, displayname, description, \"order\", color, push_topic)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 7
+ },
+ "nullable": []
+ },
+ "hash": "72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a"
+}
diff --git a/.sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json b/.sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json
deleted file mode 100644
index 3d85443..0000000
--- a/.sqlx/query-bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "INSERT INTO birthday_calendars (principal, id, displayname, push_topic)\n VALUES (?, ?, ?, ?)",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 4
- },
- "nullable": []
- },
- "hash": "bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c"
-}