Add JtxRecurringCollection for managing recurring jtx objects and exceptions#421
Add JtxRecurringCollection for managing recurring jtx objects and exceptions#421sunkup wants to merge 3 commits into
JtxRecurringCollection for managing recurring jtx objects and exceptions#421Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new JtxRecurringCollection helper to manage recurring jtx objects together with their exception rows (linked by UID/RECURID) on top of the existing JtxCollection storage API, plus supporting assertion helpers and instrumentation tests.
Changes:
- Add
JtxRecurringCollectionwith CRUD methods for “main + exceptions” and utilities to process dirty/deleted exceptions. - Add android instrumentation tests covering CRUD, cleanup behavior, and dirty/deleted exception processing.
- Extend test assertion helpers to compare
JtxObjectAndExceptionsand to apply a main jtx ID for comparisons.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| lib/src/main/kotlin/at/bitfire/synctools/test/AssertHelpers.kt | Adds assertion + helper methods for JtxObjectAndExceptions comparisons in tests. |
| lib/src/main/kotlin/at/bitfire/synctools/storage/jtx/JtxRecurringCollection.kt | Introduces the recurring “main + exceptions” storage abstraction and exception-processing helpers. |
| lib/src/androidTest/kotlin/at/bitfire/synctools/storage/jtx/JtxRecurringCollectionTest.kt | Adds instrumentation coverage for the new recurring collection behavior. |
Comments suppressed due to low confidence (2)
lib/src/main/kotlin/at/bitfire/synctools/storage/jtx/JtxRecurringCollection.kt:156
- issue (blocking):
updateJtxObjectAndExceptionsassumesidrefers to a main row, but it can silently corrupt data when an exception row ID is passed.
Because exceptions are associated by UID, passing an exception ID will cause findExceptionsByUid(existingUid) to include that same row, enqueueing a delete for id and then an update for the deleted row in the same batch (likely resulting in the row being removed and exceptions re-inserted without a main). Consider validating that the existing row has RECURID IS NULL (or at least RECURID is null in the fetched row) and throw IllegalArgumentException / return early if id is not a main object.
fun updateJtxObjectAndExceptions(id: Long, objectAndExceptions: JtxObjectAndExceptions): Long {
try {
// validate / clean up input
val cleaned = cleanUp(objectAndExceptions)
// get UID of the existing main object to find and delete its old exceptions (because
// they may be invalid for the updated event)
val existingUid = collection.getJtxObjectRow(id, arrayOf(JtxContract.JtxICalObject.UID))
?.getAsString(JtxContract.JtxICalObject.UID)
val batch = JtxBatchOperation(collection.client)
// Delete old exceptions individually by item URI. The jtx Board provider blocks
// bulk directory-level deletes of RECURID-bearing rows (adds "AND RECURID IS NULL"),
// so a bulk delete would silently be a no-op here.
// See: https://github.com/TechbeeAT/jtxBoard/blob/45a5f75693b2a50e55f2812bb5681016e50500d8/app/src/main/java/at/techbee/jtx/SyncContentProvider.kt#L197
if (existingUid != null)
for (oldException in findExceptionsByUid(existingUid))
collection.deleteJtxObject(
oldException.entityValues.getAsLong(JtxContract.JtxICalObject.ID),
batch
)
// update main object
collection.updateJtxObject(id, cleaned.main, batch)
lib/src/main/kotlin/at/bitfire/synctools/storage/jtx/JtxRecurringCollection.kt:186
- issue (blocking):
deleteJtxObjectAndExceptions()will delete whatever rowidpoints to, even if it's an exception row.
Given this API is documented as taking a main ID, it should validate that the row has RECURID IS NULL before deleting; otherwise callers can accidentally delete a single exception while leaving the main + other exceptions behind, or trigger provider-side orphan cleanup in unexpected ways.
/**
* Deletes a jtx object and all its exceptions.
*
* @param id ID of the main jtx object
*/
fun deleteJtxObjectAndExceptions(id: Long) {
try {
val batch = JtxBatchOperation(collection.client)
// delete main object; the jtx Board provider's removeOrphans() will clean up
// associated exceptions and auto-generated instances when the main is removed
collection.deleteJtxObject(id, batch)
batch.commit()
} catch (e: RemoteException) {
throw LocalStorageException("Couldn't delete jtx object $id", e)
}
}
683d413 to
ea06f65
Compare
ea06f65 to
89648f2
Compare
Introduce
JtxRecurringCollection(+tests) to handle recurrence and exception instances of jtx ical objects. See also #421 (review) .Note
Copilot suggests safeguarding against misusing exception IDs. Would be a bit of a weird mistake to make and since its extra code I am not entirely sure whether we should add these checks. Added it, but could revert that commit if this seems overengineered.