Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import world.respect.datalayer.db.school.daos.PersonQrBadgeEntityDao
import world.respect.datalayer.db.school.daos.PersonRelatedPersonEntityDao
import world.respect.datalayer.db.school.daos.PullSyncStatusEntityDao
import world.respect.datalayer.db.school.daos.SchoolAppEntityDao
import world.respect.datalayer.db.school.daos.SchoolConfigSettingEntityDao
import world.respect.datalayer.db.school.daos.WriteQueueItemEntityDao
import world.respect.datalayer.db.school.entities.AssignmentEntity
import world.respect.datalayer.db.school.entities.AssignmentLearningResourceRefEntity
Expand All @@ -59,6 +60,7 @@ import world.respect.datalayer.db.school.entities.WriteQueueItemEntity
import world.respect.datalayer.db.school.daos.SchoolPermissionGrantDao
import world.respect.datalayer.db.school.entities.ClassPermissionEntity
import world.respect.datalayer.db.school.entities.PullSyncStatusEntity
import world.respect.datalayer.db.school.entities.SchoolConfigSettingEntity
import world.respect.datalayer.db.school.entities.SchoolPermissionGrantEntity
import world.respect.datalayer.school.model.Assignment
import world.respect.datalayer.school.model.Clazz
Expand Down Expand Up @@ -105,8 +107,10 @@ import world.respect.datalayer.school.model.Report
OpdsGroupEntity::class,
OpdsFeedEntity::class,
OpdsFeedMetadataEntity::class,

SchoolConfigSettingEntity::class,
],
version = 12,
version = 13,
)
@TypeConverters(SharedConverters::class, SchoolTypeConverters::class, OpdsTypeConverters::class)
@ConstructedBy(RespectSchoolDatabaseConstructor::class)
Expand Down Expand Up @@ -162,6 +166,7 @@ abstract class RespectSchoolDatabase: RoomDatabase() {

abstract fun getOpdsGroupEntityDao(): OpdsGroupEntityDao

abstract fun getSchoolConfigSettingEntityDao(): SchoolConfigSettingEntityDao

companion object {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,24 @@ val MIGRATION_11_12 = object: Migration(11, 12) {
}
}

val MIGRATION_12_13 = object: Migration(12, 13) {
override fun migrate(connection: SQLiteConnection) {

connection.execSQL("UPDATE PersonRoleEntity SET prRoleEnum = CASE WHEN prRoleEnum = 3 THEN 4 WHEN prRoleEnum = 4 THEN 8 WHEN prRoleEnum = 5 THEN 16 ELSE prRoleEnum END WHERE prRoleEnum IN (3, 4, 5)")

connection.execSQL("UPDATE SchoolPermissionGrantEntity SET spgToRole = CASE WHEN spgToRole = 3 THEN 4 WHEN spgToRole = 4 THEN 8 WHEN spgToRole = 5 THEN 16 ELSE spgToRole END WHERE spgToRole IN (3, 4, 5)")

connection.execSQL("UPDATE InviteEntity SET iNewUserRole = CASE WHEN iNewUserRole = 3 THEN 4 WHEN iNewUserRole = 4 THEN 8 WHEN iNewUserRole = 5 THEN 16 ELSE iNewUserRole END WHERE iNewUserRole IN (3, 4, 5)")

connection.execSQL("CREATE TABLE IF NOT EXISTS `SchoolConfigSettingEntity` (`scsKey` TEXT NOT NULL, `scsValue` TEXT NOT NULL, `scsStatus` INTEGER NOT NULL, `scsLastModified` INTEGER NOT NULL, `scsStored` INTEGER NOT NULL, `scsCanReadFlags` INTEGER NOT NULL, `scsAnonCanRead` INTEGER NOT NULL, `scsCanWriteFlags` INTEGER NOT NULL, PRIMARY KEY(`scsKey`))")
}
}

Comment thread
Anugraha-sutara marked this conversation as resolved.
fun RoomDatabase.Builder<RespectSchoolDatabase>.addCommonMigrations(

): RoomDatabase.Builder<RespectSchoolDatabase> {
return this.addMigrations(MIGRATION_11_12)
return this.addMigrations(
MIGRATION_11_12,
MIGRATION_12_13,
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import kotlinx.serialization.json.Json
import world.respect.datalayer.AuthenticatedUserPrincipalId
import world.respect.datalayer.SchoolDataSourceLocal
import world.respect.datalayer.UidNumberMapper
import world.respect.datalayer.db.school.opds.OpdsPublicationDataSourceDb
import world.respect.datalayer.db.school.opds.OpdsFeedDataSourceDb
import world.respect.datalayer.db.school.AssignmentDatasourceDb
import world.respect.datalayer.db.school.ClassDatasourceDb
import world.respect.datalayer.db.school.EnrollmentDataSourceDb
Expand All @@ -18,10 +16,12 @@ import world.respect.datalayer.db.school.PersonPasswordDataSourceDb
import world.respect.datalayer.db.school.PersonQrBadgeDataSourceDb
import world.respect.datalayer.db.school.ReportDataSourceDb
import world.respect.datalayer.db.school.SchoolAppDataSourceDb
import world.respect.datalayer.db.school.SchoolConfigSettingDataSourceDb
import world.respect.datalayer.db.school.SchoolPermissionGrantDataSourceDb
import world.respect.datalayer.db.school.opds.OpdsFeedDataSourceDb
import world.respect.datalayer.db.school.opds.OpdsPublicationDataSourceDb
import world.respect.datalayer.school.AssignmentDataSourceLocal
import world.respect.datalayer.school.ClassDataSourceLocal
import world.respect.datalayer.school.DummySchoolConfigSettingsDataSource
import world.respect.datalayer.school.EnrollmentDataSourceLocal
import world.respect.datalayer.school.IndicatorDataSource
import world.respect.datalayer.school.InviteDataSourceLocal
Expand All @@ -31,11 +31,10 @@ import world.respect.datalayer.school.PersonPasswordDataSourceLocal
import world.respect.datalayer.school.PersonQrCodeBadgeDataSourceLocal
import world.respect.datalayer.school.ReportDataSourceLocal
import world.respect.datalayer.school.SchoolAppDataSourceLocal
import world.respect.datalayer.school.SchoolConfigSettingDataSource
import world.respect.datalayer.school.SchoolPermissionGrantDataSourceLocal
import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase
import world.respect.datalayer.school.opds.OpdsPublicationDataSourceLocal
import world.respect.datalayer.school.opds.OpdsFeedDataSourceLocal
import world.respect.datalayer.school.opds.OpdsPublicationDataSourceLocal
import world.respect.lib.primarykeygen.PrimaryKeyGenerator

/**
Expand Down Expand Up @@ -136,9 +135,11 @@ class SchoolDataSourceDb(
)
}

override val schoolConfigSettingDataSource: SchoolConfigSettingDataSource by lazy {
DummySchoolConfigSettingsDataSource(
defaultAppCatalogUrl = defaultAppCatalogUrl,
override val schoolConfigSettingDataSource by lazy {
SchoolConfigSettingDataSourceDb(
schoolDb = schoolDb,
authenticatedUser = authenticatedUser,
uidNumberMapper = uidNumberMapper,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package world.respect.datalayer.db.school

import androidx.room.Transactor
import androidx.room.useWriterConnection
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import world.respect.datalayer.AuthenticatedUserPrincipalId
import world.respect.datalayer.DataLoadMetaInfo
import world.respect.datalayer.DataLoadParams
import world.respect.datalayer.DataLoadState
import world.respect.datalayer.DataReadyState
import world.respect.datalayer.NoDataLoadedState
import world.respect.datalayer.UidNumberMapper
import world.respect.datalayer.db.RespectSchoolDatabase
import world.respect.datalayer.db.school.adapters.asEntity
import world.respect.datalayer.db.school.adapters.asModel
import world.respect.datalayer.exceptions.ForbiddenException
import world.respect.datalayer.school.SchoolConfigSettingDataSource
import world.respect.datalayer.school.SchoolConfigSettingDataSourceLocal
import world.respect.datalayer.school.ext.foldToFlag
import world.respect.datalayer.school.model.SchoolConfigSetting
import world.respect.datalayer.shared.maxLastModifiedOrNull
import world.respect.datalayer.shared.maxLastStoredOrNull
import kotlin.time.Clock

class SchoolConfigSettingDataSourceDb(
private val schoolDb: RespectSchoolDatabase,
private val authenticatedUser: AuthenticatedUserPrincipalId,
private val uidNumberMapper: UidNumberMapper,
) : SchoolConfigSettingDataSourceLocal {

private val authenticatedUserUidNum: Long
get() = uidNumberMapper(authenticatedUser.guid)

override suspend fun findByGuid(
params: DataLoadParams,
guid: String
): DataLoadState<SchoolConfigSetting> {
return schoolDb.getSchoolConfigSettingEntityDao().list(
authenticatedPersonUidNum = authenticatedUserUidNum,
keys = listOf(guid)
).firstOrNull()?.asModel()?.let { DataReadyState(it) } ?: NoDataLoadedState.notFound()
}

override fun listAsFlow(
loadParams: DataLoadParams,
params: SchoolConfigSettingDataSource.GetListParams
): Flow<DataLoadState<List<SchoolConfigSetting>>> {
return schoolDb.getSchoolConfigSettingEntityDao().listAsFlow(
authenticatedPersonUidNum = authenticatedUserUidNum,
keys = params.keys,
since = params.common.since?.toEpochMilliseconds() ?: 0
).map { list ->
DataReadyState(
data = list.map { it.asModel() }
)
}
}

override suspend fun list(
loadParams: DataLoadParams,
params: SchoolConfigSettingDataSource.GetListParams
): DataLoadState<List<SchoolConfigSetting>> {
val queryTime = Clock.System.now()
val data = schoolDb.getSchoolConfigSettingEntityDao().list(
authenticatedPersonUidNum = authenticatedUserUidNum,
keys = params.keys,
since = params.common.since?.toEpochMilliseconds() ?: 0
).map { it.asModel() }

return DataReadyState(
data = data,
metaInfo = DataLoadMetaInfo(
lastModified = data.maxLastModifiedOrNull()?.toEpochMilliseconds() ?: -1,
lastStored = data.maxLastStoredOrNull()?.toEpochMilliseconds() ?: -1,
consistentThrough = queryTime,
)
)
}

override suspend fun store(list: List<SchoolConfigSetting>) {
if (list.isEmpty()) return
schoolDb.useWriterConnection { con ->
con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {
list.forEach { setting ->
val lastModAndPermission = schoolDb.getSchoolConfigSettingEntityDao()
.getLastModifiedAndHasPermission(
authenticatedPersonUidNum = authenticatedUserUidNum,
key = setting.key,
canWriteRolesMask = setting.canWrite.foldToFlag()
)

if (!lastModAndPermission.hasPermission) {
throw ForbiddenException()
}
}

schoolDb.getSchoolConfigSettingEntityDao().insert(
list.map { it.copy(stored = Clock.System.now()).asEntity() }
)
}
}
}

override suspend fun updateLocal(
list: List<SchoolConfigSetting>,
forceOverwrite: Boolean
) {
if (list.isEmpty()) return
schoolDb.useWriterConnection { con ->
con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {
val toInsert = list.filter { item ->
forceOverwrite || schoolDb.getSchoolConfigSettingEntityDao()
.getLastModifiedByKey(
key = item.key
).let { it ?: 0L } < item.lastModified.toEpochMilliseconds()
}.map { it.asEntity() }

if (toInsert.isNotEmpty()) {
schoolDb.getSchoolConfigSettingEntityDao().insert(toInsert)
}
}
}
}

override suspend fun findByUidList(uids: List<String>): List<SchoolConfigSetting> {
return schoolDb.getSchoolConfigSettingEntityDao().list(
authenticatedPersonUidNum = authenticatedUserUidNum,
keys = uids
).map { it.asModel() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package world.respect.datalayer.db.school.adapters

import world.respect.datalayer.db.school.entities.SchoolConfigSettingEntity
import world.respect.datalayer.school.ext.foldToFlag
import world.respect.datalayer.school.model.PersonRoleEnum
import world.respect.datalayer.school.model.SchoolConfigSetting


fun SchoolConfigSetting.asEntity(): SchoolConfigSettingEntity {
return SchoolConfigSettingEntity(
scsKey = key,
scsValue = value,
scsStatus = status,
scsLastModified = lastModified,
scsStored = stored,
scsCanReadFlags = canRead.foldToFlag(),
scsCanWriteFlags = canWrite.foldToFlag(),
scsAnonCanRead = canRead.contains(null),
)
}

fun SchoolConfigSettingEntity.asModel(): SchoolConfigSetting {
return SchoolConfigSetting(
key = scsKey,
value = scsValue,
status = scsStatus,
lastModified = scsLastModified,
stored = scsStored,
canRead = PersonRoleEnum.unfoldFromFlag(scsCanReadFlags),
canWrite = PersonRoleEnum.unfoldFromFlag(scsCanWriteFlags)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package world.respect.datalayer.db.school.daos

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import world.respect.datalayer.db.school.entities.LastModifiedAndPermission
import world.respect.datalayer.db.school.entities.SchoolConfigSettingEntity

@Dao
interface SchoolConfigSettingEntityDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entities: List<SchoolConfigSettingEntity>)

@Query(LIST_SQL)
fun listAsFlow(
authenticatedPersonUidNum: Long,
keys: List<String>? = null,
since: Long = 0,
): Flow<List<SchoolConfigSettingEntity>>

@Query(LIST_SQL)
suspend fun list(
authenticatedPersonUidNum: Long,
keys: List<String>? = null,
since: Long = 0,
): List<SchoolConfigSettingEntity>

@Query("""
SELECT scsLastModified
FROM SchoolConfigSettingEntity
WHERE scsKey = :key
""")
suspend fun getLastModifiedByKey(key: String): Long?

@Query(GET_LAST_MODIFIED_AND_HAS_PERMISSION_SQL)
suspend fun getLastModifiedAndHasPermission(
authenticatedPersonUidNum: Long,
key: String,
canWriteRolesMask: Int = 0
): LastModifiedAndPermission

companion object {

private const val AUTHENTICATED_USER_ROLE_SQL = """
SELECT PersonRoleEntity.prRoleEnum
FROM PersonRoleEntity
WHERE PersonRoleEntity.prPersonGuidHash = :authenticatedPersonUidNum
LIMIT 1
"""

private const val READ_PERMISSION_CHECK_SQL = """
scsAnonCanRead OR (($AUTHENTICATED_USER_ROLE_SQL) & scsCanReadFlags) > 0
"""

private const val WRITE_PERMISSION_CHECK_SQL = """
(($AUTHENTICATED_USER_ROLE_SQL) & scsCanWriteFlags) > 0
"""

private const val LIST_SQL = """
SELECT SchoolConfigSettingEntity.*
FROM SchoolConfigSettingEntity
WHERE scsKey IN (:keys)
AND SchoolConfigSettingEntity.scsStored > :since
AND ($READ_PERMISSION_CHECK_SQL)
"""

private const val GET_LAST_MODIFIED_AND_HAS_PERMISSION_SQL = """
SELECT 0 AS uidNum,
(SELECT SchoolConfigSettingEntity.scsLastModified
FROM SchoolConfigSettingEntity
WHERE SchoolConfigSettingEntity.scsKey = :key) AS lastModified,
(
-- for existing records
EXISTS (
SELECT 1
FROM SchoolConfigSettingEntity
WHERE SchoolConfigSettingEntity.scsKey = :key
AND ($WRITE_PERMISSION_CHECK_SQL)
)
OR
-- for new records (using the passed mask)
(NOT EXISTS (SELECT 1 FROM SchoolConfigSettingEntity WHERE scsKey = :key)
AND ($AUTHENTICATED_USER_ROLE_SQL) & :canWriteRolesMask > 0)
) AS hasPermission
"""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package world.respect.datalayer.db.school.entities

import androidx.room.Entity
import androidx.room.PrimaryKey
import world.respect.datalayer.school.model.StatusEnum
import kotlin.time.Instant

@Entity
data class SchoolConfigSettingEntity(
@PrimaryKey
val scsKey: String,
val scsValue: String,
val scsStatus: StatusEnum,
val scsLastModified: Instant,
val scsStored: Instant,
val scsCanReadFlags: Int,
val scsAnonCanRead: Boolean,
val scsCanWriteFlags: Int,
)
Loading