From 7dcd958f2ae31ae2c87d0aa713e796b891baa778 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 09:38:56 +0200 Subject: [PATCH 1/8] [MS-1243] 'externalCredentialIds' field was added in EnrolmentEventV4 but missed in migrations. Adding migrations to introduce empty array for all old events --- .../events/event/local/EventRoomDatabase.kt | 4 +- .../local/migrations/EventMigration16to17.kt | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt index 323dc422a4..fe71bc5f01 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt @@ -13,6 +13,7 @@ import com.simprints.infra.events.event.local.migrations.EventMigration12to13 import com.simprints.infra.events.event.local.migrations.EventMigration13to14 import com.simprints.infra.events.event.local.migrations.EventMigration14to15 import com.simprints.infra.events.event.local.migrations.EventMigration15to16 +import com.simprints.infra.events.event.local.migrations.EventMigration16to17 import com.simprints.infra.events.event.local.migrations.EventMigration1to2 import com.simprints.infra.events.event.local.migrations.EventMigration2to3 import com.simprints.infra.events.event.local.migrations.EventMigration3to4 @@ -31,7 +32,7 @@ import net.zetetic.database.sqlcipher.SupportOpenHelperFactory DbEvent::class, DbEventScope::class, ], - version = 16, + version = 17, exportSchema = true, ) @TypeConverters(Converters::class) @@ -64,6 +65,7 @@ internal abstract class EventRoomDatabase : RoomDatabase() { .addMigrations(EventMigration13to14()) .addMigrations(EventMigration14to15()) .addMigrations(EventMigration15to16()) + .addMigrations(EventMigration16to17()) if (BuildConfig.DB_ENCRYPTION) { builder.openHelperFactory(factory) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt new file mode 100644 index 0000000000..6eb0e557eb --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt @@ -0,0 +1,62 @@ +package com.simprints.infra.events.event.local.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.simprints.core.tools.extentions.getStringWithColumnName +import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MIGRATION +import com.simprints.infra.logging.Simber + +/** + * Starting from 2025.4.0, EnrolmentEventV4 requires the `externalCredentialIds` field. + * This migration adds an empty `externalCredentialIds` array field to the EnrolmentEventV4 event + * + * This migration adds: + * "externalCredentialIds": [], + * before the "type" field within the payload + */ +internal class EventMigration16to17 : Migration(16, 17) { + override fun migrate(database: SupportSQLiteDatabase) { + Simber.i("Migrating room db from schema 16 to schema 17.", tag = MIGRATION) + migrateEnrolmentEventJson(database) + Simber.i("Migration from schema 16 to schema 17 done.", tag = MIGRATION) + } + + private fun migrateEnrolmentEventJson(database: SupportSQLiteDatabase) { + val eventsQuery = database.query( + "SELECT * FROM $DB_EVENT_ENTITY WHERE type = ?", + arrayOf(EVENT_TYPE_ENROLMENT_V4), + ) + eventsQuery.use { cursor -> + while (cursor.moveToNext()) { + val id = cursor.getStringWithColumnName("id") ?: continue + val jsonData = cursor.getStringWithColumnName(DB_EVENT_JSON_FIELD) ?: continue + + // Only adding if 'externalCredentialIds' field doesn't exist + if (!jsonData.contains(EXTERNAL_CREDENTIAL_IDS)) { + val migratedJson = jsonData.addExternalCredentialIdsField() + database.execSQL( + "UPDATE $DB_EVENT_ENTITY SET $DB_EVENT_JSON_FIELD = ? WHERE id = ?", + arrayOf(migratedJson, id), + ) + } + } + } + } + + private fun String.addExternalCredentialIdsField(): String { + val searchPattern = "\"type\":\"$EVENT_TYPE_ENROLMENT_V4" + val insertPosition = indexOf(searchPattern) + + if (insertPosition == -1) return this + + val newField = "\"$EXTERNAL_CREDENTIAL_IDS\":[]," + return StringBuilder(this).insert(insertPosition, newField).toString() + } + + companion object { + private const val DB_EVENT_ENTITY = "DbEvent" + private const val DB_EVENT_JSON_FIELD = "eventJson" + private const val EVENT_TYPE_ENROLMENT_V4 = "ENROLMENT_V4" + private const val EXTERNAL_CREDENTIAL_IDS = "externalCredentialIds" + } +} From a65cbcc0c6e3248d54094182f6d0d6dd48444cb4 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 10:44:05 +0200 Subject: [PATCH 2/8] [MS-1244] 'externalCredentialIds' field was added in EnrolmentEventV4 but missed in migrations. Adding migration tests --- .../17.json | 138 ++++++++++ .../local/migrations/EventMigration16to17.kt | 4 +- .../migrations/EventMigration16to17Test.kt | 255 ++++++++++++++++++ 3 files changed, 395 insertions(+), 2 deletions(-) create mode 100644 infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/17.json create mode 100644 infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt diff --git a/infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/17.json b/infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/17.json new file mode 100644 index 0000000000..2313cbd326 --- /dev/null +++ b/infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/17.json @@ -0,0 +1,138 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "d5dd0e6fc8f6d48c5f58e7b191bc8d3d", + "entities": [ + { + "tableName": "DbEvent", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `projectId` TEXT, `scopeId` TEXT, `eventJson` TEXT NOT NULL, `createdAt_unixMs` INTEGER NOT NULL, `createdAt_isTrustworthy` INTEGER NOT NULL, `createdAt_msSinceBoot` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT" + }, + { + "fieldPath": "scopeId", + "columnName": "scopeId", + "affinity": "TEXT" + }, + { + "fieldPath": "eventJson", + "columnName": "eventJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt.unixMs", + "columnName": "createdAt_unixMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.isTrustworthy", + "columnName": "createdAt_isTrustworthy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.msSinceBoot", + "columnName": "createdAt_msSinceBoot", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "DbEventScope", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `projectId` TEXT NOT NULL, `type` TEXT NOT NULL, `payloadJson` TEXT NOT NULL, `start_unixMs` INTEGER NOT NULL, `start_isTrustworthy` INTEGER NOT NULL, `start_msSinceBoot` INTEGER, `end_unixMs` INTEGER, `end_isTrustworthy` INTEGER, `end_msSinceBoot` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payloadJson", + "columnName": "payloadJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt.unixMs", + "columnName": "start_unixMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.isTrustworthy", + "columnName": "start_isTrustworthy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.msSinceBoot", + "columnName": "start_msSinceBoot", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt.unixMs", + "columnName": "end_unixMs", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt.isTrustworthy", + "columnName": "end_isTrustworthy", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt.msSinceBoot", + "columnName": "end_msSinceBoot", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd5dd0e6fc8f6d48c5f58e7b191bc8d3d')" + ] + } +} \ No newline at end of file diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt index 6eb0e557eb..d45db970f3 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt @@ -15,9 +15,9 @@ import com.simprints.infra.logging.Simber * before the "type" field within the payload */ internal class EventMigration16to17 : Migration(16, 17) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { Simber.i("Migrating room db from schema 16 to schema 17.", tag = MIGRATION) - migrateEnrolmentEventJson(database) + migrateEnrolmentEventJson(db) Simber.i("Migration from schema 16 to schema 17 done.", tag = MIGRATION) } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt new file mode 100644 index 0000000000..116364568a --- /dev/null +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt @@ -0,0 +1,255 @@ +package com.simprints.infra.events.event.local.migrations + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import com.simprints.core.tools.extentions.getStringWithColumnName +import com.simprints.core.tools.utils.randomUUID +import com.simprints.infra.events.event.local.EventRoomDatabase +import io.mockk.spyk +import io.mockk.verify +import org.json.JSONObject +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class EventMigration16to17Test { + @get:Rule + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + EventRoomDatabase::class.java, + ) + + @Test + @Throws(IOException::class) + fun `validate end to end migration is successful`() { + val eventId = randomUUID() + + setupV16DbWithEvent(eventId) + + val db = helper.runMigrationsAndValidate(TEST_DB, 17, true, EventMigration16to17()) + + val eventJson = MigrationTestingTools + .retrieveCursorWithEventById(db, eventId) + .getStringWithColumnName("eventJson")!! + + val jsonObject = JSONObject(eventJson) + val payload = jsonObject.getJSONObject("payload") + assertThat(eventJson).contains("\"$EXTERNAL_CREDENTIAL_IDS_JSON_KEY\":[]") + assertThat(payload.has(EXTERNAL_CREDENTIAL_IDS_JSON_KEY)).isTrue() + assertThat(payload.getJSONArray(EXTERNAL_CREDENTIAL_IDS_JSON_KEY).length()).isEqualTo(0) + + db.close() + } + + @Test + fun `validate migration is called`() { + val migrationSpy = spyk(EventMigration16to17()) + + setupV16DbWithEvent(randomUUID()) + helper.runMigrationsAndValidate(TEST_DB, 17, true, migrationSpy) + + verify(exactly = 1) { migrationSpy.migrate(any()) } + } + + @Test + fun `validate all ENROLMENT_V4 events are migrated`() { + val eventId1 = randomUUID() + val eventId2 = randomUUID() + + setupV16DbWithEvent(eventId1, eventId2) + val db = helper.runMigrationsAndValidate(TEST_DB, 17, true, EventMigration16to17()) + + MigrationTestingTools.retrieveCursorWithEventById(db, eventId1).use { cursor -> + val eventJson = cursor.getStringWithColumnName("eventJson") + assertThat(eventJson).contains("\"$EXTERNAL_CREDENTIAL_IDS_JSON_KEY\":[]") + } + + MigrationTestingTools.retrieveCursorWithEventById(db, eventId2).use { cursor -> + val eventJson = cursor.getStringWithColumnName("eventJson") + assertThat(eventJson).contains("\"$EXTERNAL_CREDENTIAL_IDS_JSON_KEY\":[]") + } + + db.close() + } + + @Test + fun `validate migration query is called`() { + val migrationSpy = spyk(EventMigration16to17()) + + val db = spyk(setupV16DbWithEvent(randomUUID(), close = false)) + migrationSpy.migrate(db) + + verify(atLeast = 1) { db.query(any()) } + db.close() + } + + @Test + fun `validate migration does not add field if it already exists`() { + val eventId = randomUUID() + + setupV16DbWithEventWithExternalCredentialIds(eventId) + val db = helper.runMigrationsAndValidate(TEST_DB, 17, true, EventMigration16to17()) + + val eventJson = MigrationTestingTools + .retrieveCursorWithEventById(db, eventId) + .getStringWithColumnName("eventJson")!! + + // Verify the field appears only once + val firstIndex = eventJson.indexOf("\"$EXTERNAL_CREDENTIAL_IDS_JSON_KEY\"") + val lastIndex = eventJson.lastIndexOf("\"$EXTERNAL_CREDENTIAL_IDS_JSON_KEY\"") + assertThat(firstIndex).isEqualTo(lastIndex) + assertThat(eventJson).contains(EXTERNAL_CREDENTIAL_IDS_JSON_FIELD) + + db.close() + } + + @Test + fun `validate non-ENROLMENT_V4 events are not modified`() { + val eventId = randomUUID() + + setupV16DbWithNonEnrolmentEvent(eventId) + val db = helper.runMigrationsAndValidate(TEST_DB, 17, true, EventMigration16to17()) + + val eventJson = MigrationTestingTools + .retrieveCursorWithEventById(db, eventId) + .getStringWithColumnName("eventJson")!! + + assertThat(eventJson).doesNotContain(EXTERNAL_CREDENTIAL_IDS_JSON_KEY) + db.close() + } + + private fun createEnrolmentEvent( + id: String, + addCredentialIds: Boolean, + ) = ContentValues().apply { + val externalCredentialIds = if (addCredentialIds) { + EXTERNAL_CREDENTIAL_IDS_JSON_FIELD + } else { + "" + } + put("id", id) + put("type", "ENROLMENT_V4") + put("createdAt_unixMs", 123) + put("createdAt_isTrustworthy", 0) + put("projectId", "9WNCAbWVNrxttDe5hgwb") + put("scopeId", "2bdc1145") + val unversionedEnrolmentEvent = + """ + { + "id":"$id", + "payload":{ + "createdAt":{ + "ms":1762805893067, + "isTrustworthy":true, + "msSinceBoot":35002538 + }, + "eventVersion":4, + "subjectId":"74639420-8e77-4a40-a452-280f295f147f", + "projectId":"FW1jU2kjy1cV9RWXdosN", + "moduleId":{ + "className":"TokenizableString.Tokenized", + "value":"AV50RNsaMs9jpoHwcXZqir1uB3St0vsexOpixA==" + }, + "attendantId":{ + "className":"TokenizableString.Tokenized", + "value":"AQYk7uBNIkhgOVGR3f/0HTjX/LRk0fKi+g==" + }, + "biometricReferenceIds":[ + "b12815ff-a4bc-4d7f-ae88-608640c7138d" + ], + $externalCredentialIds + "type":"ENROLMENT_V4" + }, + "type":"ENROLMENT_V4", + "scopeId":"c33023a1-d335-4310-b088-81575daafea3", + "projectId":"FW1jU2kjy1cV9RWXdosN" + } + """.trimIndent() + put("eventJson", unversionedEnrolmentEvent) + } + + private fun createNonEnrolmentEvent(id: String) = ContentValues().apply { + put("id", id) + put("type", "INTENT_PARSING") + put("createdAt_unixMs", 123) + put("createdAt_isTrustworthy", 0) + put("projectId", "9WNCAbWVNrxttDe5hgwb") + put("scopeId", "2bdc1145") + put( + "eventJson", + """ + { + "id":"d256e644-ce5b-4ec5-8909-3a372a930206", + "projectId":"9WNCAbWVNrxttDe5hgwb", + "sessionId":"2bdc1145-cbec-4e6a-ac8a-61c1e5b53bb4", + "payload":{ + "createdAt":{ + "ms":1706534485916, + "isTrustworthy":false, + "msSinceBoot":null + }, + "eventVersion":2, + "integration":"STANDARD", + "type":"INTENT_PARSING", + "endedAt":{ + "ms":1706534528165, + "isTrustworthy":false, + "msSinceBoot":null + } + }, + "type":"INTENT_PARSING" + } + """.trimIndent(), + ) + } + + private fun setupV16DbWithEvent( + vararg eventId: String, + close: Boolean = true, + ): SupportSQLiteDatabase = helper.createDatabase(TEST_DB, 16).apply { + eventId.forEach { id -> + val event = createEnrolmentEvent(id, addCredentialIds = false) + this.insert("DbEvent", SQLiteDatabase.CONFLICT_NONE, event) + } + if (close) { + close() + } + } + + private fun setupV16DbWithEventWithExternalCredentialIds( + eventId: String, + close: Boolean = true, + ): SupportSQLiteDatabase = helper.createDatabase(TEST_DB, 16).apply { + val event = createEnrolmentEvent(eventId, addCredentialIds = true) + this.insert("DbEvent", SQLiteDatabase.CONFLICT_NONE, event) + if (close) { + close() + } + } + + private fun setupV16DbWithNonEnrolmentEvent( + eventId: String, + close: Boolean = true, + ): SupportSQLiteDatabase = helper.createDatabase(TEST_DB, 16).apply { + val event = createNonEnrolmentEvent(eventId) + this.insert("DbEvent", SQLiteDatabase.CONFLICT_NONE, event) + if (close) { + close() + } + } + + companion object { + private const val TEST_DB = "test" + private const val EXTERNAL_CREDENTIAL_IDS_JSON_KEY = "externalCredentialIds" + private const val EXTERNAL_CREDENTIAL_IDS_JSON_FIELD = + "\"$EXTERNAL_CREDENTIAL_IDS_JSON_KEY\": [\"74639420-8e77-4a40-a452-280f295f147f\"]\"," + } +} From 21128927d04c94db8ffabc7b7424684ae9a6b1e0 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 11:11:52 +0200 Subject: [PATCH 3/8] [MS-1244] changing Enrolment event search pattern --- .../infra/events/event/local/migrations/EventMigration16to17.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt index d45db970f3..922a77eebc 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt @@ -44,7 +44,7 @@ internal class EventMigration16to17 : Migration(16, 17) { } private fun String.addExternalCredentialIdsField(): String { - val searchPattern = "\"type\":\"$EVENT_TYPE_ENROLMENT_V4" + val searchPattern = "\"type\":\"$EVENT_TYPE_ENROLMENT_V4\"" val insertPosition = indexOf(searchPattern) if (insertPosition == -1) return this From 5def67316e22050f1aba65945d4d8c279d8c5c0d Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 11:12:12 +0200 Subject: [PATCH 4/8] [MS-1244] Updating EventMigrationTest to include EventMigration16to17 --- .../infra/events/event/local/migrations/EventMigrationTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt index 13f3e1e9f1..493fc925cc 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt @@ -113,6 +113,7 @@ class EventMigrationTest { EventMigration13to14(), EventMigration14to15(), EventMigration15to16(), + EventMigration16to17(), ) val tokenizeSerializationModule = SimpleModule().apply { addSerializer(TokenizableString::class.java, TokenizationClassNameSerializer()) From 5b32542e6d3b7142cba4339fda4b35b38bd5edad Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 11:16:58 +0200 Subject: [PATCH 5/8] [MS-1244] Changing log level to 'error' in EventUpSyncTask::getClosedScopesForType so that we have a Crashlytics non-fatal recorded in case any event fails to be parsed --- .../simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt index 17f3964b03..c3afc6e5cc 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt @@ -261,7 +261,7 @@ internal class EventUpSyncTask @Inject constructor( .also { listOfEvents -> emit(listOfEvents.size) } } catch (ex: Exception) { if (ex is JsonParseException || ex is JsonMappingException) { - Simber.i("Failed to un-marshal events", ex, tag = SYNC) + Simber.e("Failed to un-marshal events", ex, tag = SYNC) } else { throw ex } From b41038a9a3c3d471b2b83b5bf077798dbacb6e42 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 11:22:47 +0200 Subject: [PATCH 6/8] [MS-1244] Updating migration tests from 16 to 17 --- .../infra/events/event/local/migrations/EventMigrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt index 493fc925cc..feafb7444b 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigrationTest.kt @@ -47,7 +47,7 @@ class EventMigrationTest { } close() } - val db = helper.runMigrationsAndValidate(TEST_DB, 16, true, *ALL_MIGRATIONS) + val db = helper.runMigrationsAndValidate(TEST_DB, 17, true, *ALL_MIGRATIONS) db.query("SELECT * FROM $TABLE_NAME").use { cursor -> while (cursor.moveToNext()) { val eventJson = cursor.getStringWithColumnName("eventJson")!! From 39bed4f83f357c90537ba2c6993716bba12f5411 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 12:36:22 +0200 Subject: [PATCH 7/8] [MS-1244] When migrating from Event DB version 16 to 17, using JSONObject class to deserialize the even string, and add missing 'externalCredentialIds' field to the payload --- .../local/migrations/EventMigration16to17.kt | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt index 922a77eebc..d57f666933 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt @@ -5,6 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase import com.simprints.core.tools.extentions.getStringWithColumnName import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MIGRATION import com.simprints.infra.logging.Simber +import org.json.JSONArray +import org.json.JSONObject /** * Starting from 2025.4.0, EnrolmentEventV4 requires the `externalCredentialIds` field. @@ -31,32 +33,35 @@ internal class EventMigration16to17 : Migration(16, 17) { val id = cursor.getStringWithColumnName("id") ?: continue val jsonData = cursor.getStringWithColumnName(DB_EVENT_JSON_FIELD) ?: continue - // Only adding if 'externalCredentialIds' field doesn't exist - if (!jsonData.contains(EXTERNAL_CREDENTIAL_IDS)) { - val migratedJson = jsonData.addExternalCredentialIdsField() - database.execSQL( - "UPDATE $DB_EVENT_ENTITY SET $DB_EVENT_JSON_FIELD = ? WHERE id = ?", - arrayOf(migratedJson, id), + try { + val jsonObject = JSONObject(jsonData) + val payload = jsonObject.optJSONObject(PAYLOAD_JSON_FIELD) ?: continue + + // Only adding if 'externalCredentialIds' field doesn't exist + if (!payload.has(EXTERNAL_CREDENTIAL_IDS_JSON_FIELD)) { + payload.put(EXTERNAL_CREDENTIAL_IDS_JSON_FIELD, JSONArray()) + val migratedJson = jsonObject.toString() + database.execSQL( + "UPDATE $DB_EVENT_ENTITY SET $DB_EVENT_JSON_FIELD = ? WHERE id = ?", + arrayOf(migratedJson, id), + ) + } + } catch (e: Exception) { + Simber.e( + "Failed to migrate room db from schema 16 to schema 17.", + e, + tag = MIGRATION, ) } } } } - private fun String.addExternalCredentialIdsField(): String { - val searchPattern = "\"type\":\"$EVENT_TYPE_ENROLMENT_V4\"" - val insertPosition = indexOf(searchPattern) - - if (insertPosition == -1) return this - - val newField = "\"$EXTERNAL_CREDENTIAL_IDS\":[]," - return StringBuilder(this).insert(insertPosition, newField).toString() - } - companion object { private const val DB_EVENT_ENTITY = "DbEvent" private const val DB_EVENT_JSON_FIELD = "eventJson" private const val EVENT_TYPE_ENROLMENT_V4 = "ENROLMENT_V4" - private const val EXTERNAL_CREDENTIAL_IDS = "externalCredentialIds" + private const val EXTERNAL_CREDENTIAL_IDS_JSON_FIELD = "externalCredentialIds" + private const val PAYLOAD_JSON_FIELD = "payload" } } From b7b4f66664ff4afe4f0d67bf9276adbf5e16df12 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Nov 2025 12:53:20 +0200 Subject: [PATCH 8/8] [MS-1244] Updating test consistency and documentation --- .../infra/events/event/local/migrations/EventMigration16to17.kt | 2 +- .../events/event/local/migrations/EventMigration16to17Test.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt index d57f666933..545ff4fc37 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17.kt @@ -14,7 +14,7 @@ import org.json.JSONObject * * This migration adds: * "externalCredentialIds": [], - * before the "type" field within the payload + * to the payload object. */ internal class EventMigration16to17 : Migration(16, 17) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt index 116364568a..8038370fb0 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration16to17Test.kt @@ -187,7 +187,7 @@ class EventMigration16to17Test { "eventJson", """ { - "id":"d256e644-ce5b-4ec5-8909-3a372a930206", + "id":"$id", "projectId":"9WNCAbWVNrxttDe5hgwb", "sessionId":"2bdc1145-cbec-4e6a-ac8a-61c1e5b53bb4", "payload":{