2023-01-17 12:21:29 +01:00
import { ElementEntity , ListElementEntity , SomeEntity , TypeModel } from "../../common/EntityTypes.js"
2022-04-20 10:39:52 +02:00
import {
elementIdPart ,
firstBiggerThanSecond ,
GENERATED_MAX_ID ,
GENERATED_MIN_ID ,
getElementId ,
listIdPart ,
2022-12-27 15:37:40 +01:00
timestampToGeneratedId ,
2022-04-20 10:39:52 +02:00
} from "../../common/utils/EntityUtils.js"
2022-12-27 15:37:40 +01:00
import { CacheStorage , expandId , ExposedCacheStorage , LastUpdateTime } from "../rest/DefaultEntityRestCache.js"
2022-01-12 14:43:01 +01:00
import * as cborg from "cborg"
2022-12-27 15:37:40 +01:00
import { EncodeOptions , Token , Type } from "cborg"
2023-08-16 10:47:09 +02:00
import {
assert ,
assertNotNull ,
DAY_IN_MILLIS ,
getTypeId ,
groupByAndMap ,
groupByAndMapUniquely ,
mapNullable ,
splitInChunks ,
TypeRef ,
} from "@tutao/tutanota-utils"
2022-12-27 15:37:40 +01:00
import { isDesktop , isOfflineStorageAvailable , isTest } from "../../common/Env.js"
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
import { modelInfos , resolveTypeReference } from "../../common/EntityFunctions.js"
2023-02-07 10:18:22 +01:00
import { AccountType , OFFLINE_STORAGE_DEFAULT_TIME_RANGE_DAYS } from "../../common/TutanotaConstants.js"
2022-12-27 15:37:40 +01:00
import { DateProvider } from "../../common/DateProvider.js"
2024-01-08 17:14:09 +01:00
import { TokenOrNestedTokens } from "cborg/interface"
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
import {
CalendarEventTypeRef ,
FileTypeRef ,
MailBodyTypeRef ,
MailDetailsBlobTypeRef ,
MailDetailsDraftTypeRef ,
MailFolderTypeRef ,
MailHeadersTypeRef ,
MailTypeRef ,
} from "../../entities/tutanota/TypeRefs.js"
2022-12-27 15:37:40 +01:00
import { UserTypeRef } from "../../entities/sys/TypeRefs.js"
import { OfflineStorageMigrator } from "./OfflineStorageMigrator.js"
import { CustomCacheHandlerMap , CustomCalendarEventCacheHandler } from "../rest/CustomCacheHandler.js"
import { EntityRestClient } from "../rest/EntityRestClient.js"
import { InterWindowEventFacadeSendDispatcher } from "../../../native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
import { SqlCipherFacade } from "../../../native/common/generatedipc/SqlCipherFacade.js"
2023-08-15 16:43:50 +02:00
import { FormattedQuery , SqlValue , TaggedSqlValue , tagSqlValue , untagSqlObject } from "./SqlValue.js"
2023-01-12 14:54:42 +01:00
import { FolderSystem } from "../../common/mail/FolderSystem.js"
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
import { isDetailsDraft , isLegacyMail } from "../../common/MailWrapper.js"
import { Type as TypeId } from "../../common/EntityConstants.js"
2023-01-04 10:54:28 +01:00
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
2023-01-12 14:56:07 +01:00
import { isSpamOrTrashFolder } from "../../common/mail/CommonMailUtils.js"
2022-01-12 14:43:01 +01:00
2023-08-16 10:47:09 +02:00
/ * *
* this is the value of SQLITE_MAX_VARIABLE_NUMBER in sqlite3 . c
* it may change if the sqlite version is updated .
* * /
const MAX_SAFE_SQL_VARS = 32766
2022-01-12 14:43:01 +01:00
function dateEncoder ( data : Date , typ : string , options : EncodeOptions ) : TokenOrNestedTokens | null {
2022-10-13 13:29:00 +02:00
const time = data . getTime ( )
2022-01-12 14:43:01 +01:00
return [
// https://datatracker.ietf.org/doc/rfc8943/
new Token ( Type . tag , 100 ) ,
2022-12-27 15:37:40 +01:00
new Token ( time < 0 ? Type.negint : Type.uint , time ) ,
2022-01-12 14:43:01 +01:00
]
}
function dateDecoder ( bytes : number ) : Date {
return new Date ( bytes )
}
2022-12-27 15:37:40 +01:00
export const customTypeEncoders : { [ typeName : string ] : typeof dateEncoder } = Object . freeze ( {
Date : dateEncoder ,
2022-01-12 14:43:01 +01:00
} )
type TypeDecoder = ( _ : any ) = > any
export const customTypeDecoders : Array < TypeDecoder > = ( ( ) = > {
const tags : Array < TypeDecoder > = [ ]
tags [ 100 ] = dateDecoder
return tags
} ) ( )
2022-10-21 15:53:39 +02:00
/ * *
* For each of these keys we track the current version in the database .
* The keys are different model versions ( because we need to migrate the data with certain model version changes ) and "offline" key which is used to track
* migrations that are needed for other reasons e . g . if DB structure changes or if we need to invalidate some tables .
* /
export type VersionMetadataBaseKey = keyof typeof modelInfos | "offline"
2022-04-20 10:39:52 +02:00
2022-10-21 15:53:39 +02:00
type VersionMetadataEntries = {
2022-04-20 10:39:52 +02:00
// Yes this is cursed, give me a break
2022-10-21 15:53:39 +02:00
[ P in VersionMetadataBaseKey as ` ${ P } -version ` ] : number
2022-04-20 10:39:52 +02:00
}
2022-10-21 15:53:39 +02:00
export interface OfflineDbMeta extends VersionMetadataEntries {
2022-12-27 15:37:40 +01:00
lastUpdateTime : number
2022-04-20 10:39:52 +02:00
timeRangeDays : number
}
2022-08-11 16:38:53 +02:00
const TableDefinitions = Object . freeze ( {
2022-10-21 15:53:39 +02:00
// plus ownerGroup added in a migration
2022-12-27 15:37:40 +01:00
list_entities :
"type TEXT NOT NULL, listId TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, listId, elementId)" ,
2022-10-21 15:53:39 +02:00
// plus ownerGroup added in a migration
element_entities : "type TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, elementId)" ,
2022-08-11 16:38:53 +02:00
ranges : "type TEXT NOT NULL, listId TEXT NOT NULL, lower TEXT NOT NULL, upper TEXT NOT NULL, PRIMARY KEY (type, listId)" ,
lastUpdateBatchIdPerGroupId : "groupId TEXT NOT NULL, batchId TEXT NOT NULL, PRIMARY KEY (groupId)" ,
metadata : "key TEXT NOT NULL, value BLOB, PRIMARY KEY (key)" ,
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
blob_element_entities :
"type TEXT NOT NULL, listId TEXT NOT NULL, elementId TEXT NOT NULL, ownerGroup TEXT, entity BLOB NOT NULL, PRIMARY KEY (type, listId, elementId)" ,
2022-08-11 16:38:53 +02:00
} as const )
2022-12-27 15:37:40 +01:00
type Range = { lower : string ; upper : string }
2022-08-11 16:38:53 +02:00
2022-10-21 15:53:39 +02:00
export interface OfflineStorageInitArgs {
2022-12-27 15:37:40 +01:00
userId : Id
databaseKey : Uint8Array
timeRangeDays : number | null
2022-10-21 15:53:39 +02:00
forceNewDatabase : boolean
}
2022-05-12 17:06:57 +02:00
export class OfflineStorage implements CacheStorage , ExposedCacheStorage {
2022-07-04 14:55:17 +02:00
private customCacheHandler : CustomCacheHandlerMap | null = null
2022-10-21 15:53:39 +02:00
private userId : Id | null = null
2022-11-28 17:38:17 +01:00
private timeRangeDays : number | null = null
2022-02-10 16:32:47 +01:00
2022-01-12 14:43:01 +01:00
constructor (
2022-08-11 16:38:53 +02:00
private readonly sqlCipherFacade : SqlCipherFacade ,
2022-07-20 15:28:38 +02:00
private readonly interWindowEventSender : InterWindowEventFacadeSendDispatcher ,
2022-04-20 10:39:52 +02:00
private readonly dateProvider : DateProvider ,
2022-05-17 17:40:44 +02:00
private readonly migrator : OfflineStorageMigrator ,
2022-01-12 14:43:01 +01:00
) {
2022-02-10 16:32:47 +01:00
assert ( isOfflineStorageAvailable ( ) || isTest ( ) , "Offline storage is not available." )
}
2022-05-17 17:40:44 +02:00
/ * *
* @return { boolean } whether the database was newly created or not
* /
2022-12-27 15:37:40 +01:00
async init ( { userId , databaseKey , timeRangeDays , forceNewDatabase } : OfflineStorageInitArgs ) : Promise < boolean > {
2022-10-21 15:53:39 +02:00
this . userId = userId
2022-11-28 17:38:17 +01:00
this . timeRangeDays = timeRangeDays
2022-07-20 15:28:38 +02:00
if ( forceNewDatabase ) {
2022-08-11 16:38:53 +02:00
if ( isDesktop ( ) ) {
await this . interWindowEventSender . localUserDataInvalidated ( userId )
}
await this . sqlCipherFacade . deleteDb ( userId )
2022-07-20 15:28:38 +02:00
}
2022-11-28 17:38:17 +01:00
// We open database here, and it is closed in the native side when the window is closed or the page is reloaded
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . openDb ( userId , databaseKey )
await this . createTables ( )
2023-01-04 10:54:28 +01:00
try {
await this . migrator . migrate ( this , this . sqlCipherFacade )
} catch ( e ) {
2023-01-04 16:54:58 +01:00
if ( e instanceof OutOfSyncError ) {
2023-02-02 14:17:02 +01:00
console . warn ( "Offline db is out of sync!" , e )
2023-01-04 16:54:58 +01:00
await this . recreateDbFile ( userId , databaseKey )
2023-02-02 14:17:02 +01:00
await this . migrator . migrate ( this , this . sqlCipherFacade )
2023-01-04 16:54:58 +01:00
} else {
2023-01-04 10:54:28 +01:00
throw e
}
}
2022-05-17 17:40:44 +02:00
// if nothing is written here, it means it's a new database
2024-01-22 10:42:11 +01:00
return ( await this . getLastUpdateTime ( ) ) . type === "never"
2023-01-04 10:54:28 +01:00
}
2022-11-28 17:38:17 +01:00
2023-01-04 10:54:28 +01:00
private async recreateDbFile ( userId : string , databaseKey : Uint8Array ) : Promise < void > {
console . log ( ` recreating DB file for userId ${ userId } ` )
await this . sqlCipherFacade . closeDb ( )
await this . sqlCipherFacade . deleteDb ( userId )
await this . sqlCipherFacade . openDb ( userId , databaseKey )
await this . createTables ( )
2022-02-10 16:32:47 +01:00
}
2022-08-15 14:22:44 +02:00
/ * *
* currently , we close DBs from the native side ( mainly on things like reload and on android ' s onDestroy )
* /
2022-07-20 15:28:38 +02:00
async deinit() {
2022-10-21 15:53:39 +02:00
this . userId = null
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . closeDb ( )
2022-07-20 15:28:38 +02:00
}
2022-08-11 16:38:53 +02:00
async deleteIfExists ( typeRef : TypeRef < SomeEntity > , listId : Id | null , elementId : Id ) : Promise < void > {
const type = getTypeId ( typeRef )
2023-01-17 12:21:29 +01:00
let typeModel : TypeModel
try {
typeModel = await resolveTypeReference ( typeRef )
} catch ( e ) {
// prevent failed lookup for BlobToFileMapping - this catch block can be removed after May 2023
console . log ( "couldn't resolve typeRef " , typeRef )
return
}
2023-08-15 16:43:50 +02:00
let formattedQuery
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` DELETE FROM element_entities WHERE type = ${ type } AND elementId = ${ elementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` DELETE FROM list_entities WHERE type = ${ type } AND listId = ${ listId } AND elementId = ${ elementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` DELETE FROM blob_element_entities WHERE type = ${ type } AND listId = ${ listId } AND elementId = ${ elementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
default :
throw new Error ( "must be a persistent type" )
2022-02-10 16:32:47 +01:00
}
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2022-01-12 14:43:01 +01:00
}
2023-02-02 17:21:34 +01:00
async deleteAllOfType ( typeRef : TypeRef < SomeEntity > ) : Promise < void > {
const type = getTypeId ( typeRef )
let typeModel : TypeModel
try {
typeModel = await resolveTypeReference ( typeRef )
} catch ( e ) {
// prevent failed lookup for BlobToFileMapping - this catch block can be removed after May 2023
console . log ( "couldn't resolve typeRef " , typeRef )
return
}
2023-08-15 16:43:50 +02:00
let formattedQuery
2023-02-02 17:21:34 +01:00
switch ( typeModel . type ) {
case TypeId . Element :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` DELETE FROM element_entities WHERE type = ${ type } `
2023-02-02 17:21:34 +01:00
break
case TypeId . ListElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` DELETE FROM list_entities WHERE type = ${ type } `
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2023-03-29 14:46:04 +02:00
await this . deleteAllRangesForType ( type )
return
2023-02-02 17:21:34 +01:00
case TypeId . BlobElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` DELETE FROM blob_element_entities WHERE type = ${ type } `
2023-02-02 17:21:34 +01:00
break
default :
throw new Error ( "must be a persistent type" )
}
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2023-02-02 17:21:34 +01:00
}
2023-03-29 14:46:04 +02:00
private async deleteAllRangesForType ( type : string ) : Promise < void > {
const { query , params } = sql ` DELETE FROM ranges WHERE type = ${ type } `
await this . sqlCipherFacade . run ( query , params )
}
2022-08-11 16:38:53 +02:00
async get < T extends SomeEntity > ( typeRef : TypeRef < T > , listId : Id | null , elementId : Id ) : Promise < T | null > {
const type = getTypeId ( typeRef )
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const typeModel = await resolveTypeReference ( typeRef )
2023-08-15 16:43:50 +02:00
let formattedQuery
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` SELECT entity from element_entities WHERE type = ${ type } AND elementId = ${ elementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` SELECT entity from list_entities WHERE type = ${ type } AND listId = ${ listId } AND elementId = ${ elementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` SELECT entity from blob_element_entities WHERE type = ${ type } AND listId = ${ listId } AND elementId = ${ elementId } `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
default :
throw new Error ( "must be a persistent type" )
2022-08-11 16:38:53 +02:00
}
2023-08-15 16:43:50 +02:00
const result = await this . sqlCipherFacade . get ( formattedQuery . query , formattedQuery . params )
2022-12-27 15:37:40 +01:00
return result ? . entity ? this . deserialize ( typeRef , result . entity . value as Uint8Array ) : null
2022-01-12 14:43:01 +01:00
}
async getIdsInRange < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < Array < Id > > {
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
const range = await this . getRange ( type , listId )
if ( range == null ) {
throw new Error ( ` no range exists for ${ type } and list ${ listId } ` )
}
2022-12-27 15:37:40 +01:00
const { lower , upper } = range
const { query , params } = sql ` SELECT elementId FROM list_entities
2022-08-11 16:38:53 +02:00
WHERE type = $ { type }
AND listId = $ { listId }
AND ( elementId = $ { lower }
OR $ { firstIdBigger ( "elementId" , lower ) } )
AND NOT ( $ { firstIdBigger ( "elementId" , upper ) } ) `
const rows = await this . sqlCipherFacade . all ( query , params )
return rows . map ( ( row ) = > row . elementId . value as string )
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async getRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < Range | null > {
return this . getRange ( getTypeId ( typeRef ) , listId )
2022-01-12 14:43:01 +01:00
}
async isElementIdInCacheRange < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , id : Id ) : Promise < boolean > {
const range = await this . getRangeForList ( typeRef , listId )
2022-12-27 15:37:40 +01:00
return range != null && ! firstBiggerThanSecond ( id , range . upper ) && ! firstBiggerThanSecond ( range . lower , id )
2022-01-12 14:43:01 +01:00
}
async provideFromRange < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , start : Id , count : number , reverse : boolean ) : Promise < T [ ] > {
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2023-08-15 16:43:50 +02:00
let formattedQuery
2022-08-11 16:38:53 +02:00
if ( reverse ) {
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` SELECT entity FROM list_entities WHERE type = ${ type } AND listId = ${ listId } AND ${ firstIdBigger (
2022-12-27 15:37:40 +01:00
start ,
"elementId" ,
) } ORDER BY LENGTH ( elementId ) DESC , elementId DESC LIMIT $ { count } `
2022-08-11 16:38:53 +02:00
} else {
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` SELECT entity FROM list_entities WHERE type = ${ type } AND listId = ${ listId } AND ${ firstIdBigger (
2022-12-27 15:37:40 +01:00
"elementId" ,
start ,
) } ORDER BY LENGTH ( elementId ) ASC , elementId ASC LIMIT $ { count } `
2022-08-11 16:38:53 +02:00
}
2023-08-15 16:43:50 +02:00
const { query , params } = formattedQuery
2022-08-11 16:38:53 +02:00
const serializedList : ReadonlyArray < Record < string , TaggedSqlValue > > = await this . sqlCipherFacade . all ( query , params )
2022-12-27 15:37:40 +01:00
return this . deserializeList (
typeRef ,
serializedList . map ( ( r ) = > r . entity . value as Uint8Array ) ,
)
2022-01-12 14:43:01 +01:00
}
async put ( originalEntity : SomeEntity ) : Promise < void > {
const serializedEntity = this . serialize ( originalEntity )
2022-12-27 15:37:40 +01:00
const { listId , elementId } = expandId ( originalEntity . _id )
2022-08-11 16:38:53 +02:00
const type = getTypeId ( originalEntity . _type )
2022-10-21 15:53:39 +02:00
const ownerGroup = originalEntity . _ownerGroup
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const typeModel = await resolveTypeReference ( originalEntity . _type )
2023-08-15 16:43:50 +02:00
let formattedQuery : FormattedQuery
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` INSERT OR REPLACE INTO element_entities (type, elementId, ownerGroup, entity) VALUES ( ${ type } , ${ elementId } , ${ ownerGroup } , ${ serializedEntity } ) `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` INSERT OR REPLACE INTO list_entities (type, listId, elementId, ownerGroup, entity) VALUES ( ${ type } , ${ listId } , ${ elementId } , ${ ownerGroup } , ${ serializedEntity } ) `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2023-08-15 16:43:50 +02:00
formattedQuery = sql ` INSERT OR REPLACE INTO blob_element_entities (type, listId, elementId, ownerGroup, entity) VALUES ( ${ type } , ${ listId } , ${ elementId } , ${ ownerGroup } , ${ serializedEntity } ) `
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
break
default :
throw new Error ( "must be a persistent type" )
}
2023-08-15 16:43:50 +02:00
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
2022-01-12 14:43:01 +01:00
}
async setLowerRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , id : Id ) : Promise < void > {
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` UPDATE ranges SET lower = ${ id } WHERE type = ${ type } AND listId = ${ listId } `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
async setUpperRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , id : Id ) : Promise < void > {
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` UPDATE ranges SET upper = ${ id } WHERE type = ${ type } AND listId = ${ listId } `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
async setNewRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , lower : Id , upper : Id ) : Promise < void > {
2022-08-11 16:38:53 +02:00
const type = getTypeId ( typeRef )
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` INSERT OR REPLACE INTO ranges VALUES ( ${ type } , ${ listId } , ${ lower } , ${ upper } ) `
2022-08-11 16:38:53 +02:00
return this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async getLastBatchIdForGroup ( groupId : Id ) : Promise < Id | null > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT batchId from lastUpdateBatchIdPerGroupId WHERE groupId = ${ groupId } `
const row = ( await this . sqlCipherFacade . get ( query , params ) ) as { batchId : TaggedSqlValue } | null
2022-08-11 16:38:53 +02:00
return ( row ? . batchId ? . value ? ? null ) as Id | null
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async putLastBatchIdForGroup ( groupId : Id , batchId : Id ) : Promise < void > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` INSERT OR REPLACE INTO lastUpdateBatchIdPerGroupId VALUES ( ${ groupId } , ${ batchId } ) `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2022-09-07 17:09:25 +02:00
async getLastUpdateTime ( ) : Promise < LastUpdateTime > {
const time = await this . getMetadata ( "lastUpdateTime" )
2022-12-27 15:37:40 +01:00
return time ? { type : "recorded" , time } : { type : "never" }
2022-04-20 10:39:52 +02:00
}
2022-08-11 16:38:53 +02:00
async putLastUpdateTime ( ms : number ) : Promise < void > {
await this . putMetadata ( "lastUpdateTime" , ms )
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async purgeStorage ( ) : Promise < void > {
for ( let name of Object . keys ( TableDefinitions ) ) {
await this . sqlCipherFacade . run ( ` DELETE FROM ${ name } ` , [ ] )
}
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async deleteRange ( typeRef : TypeRef < unknown > , listId : string ) : Promise < void > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` DELETE FROM ranges WHERE type = ${ getTypeId ( typeRef ) } AND listId = ${ listId } `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
2022-01-12 14:43:01 +01:00
}
2022-08-11 16:38:53 +02:00
async getListElementsOfType < T extends ListElementEntity > ( typeRef : TypeRef < T > ) : Promise < Array < T > > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT entity from list_entities WHERE type = ${ getTypeId ( typeRef ) } `
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
return this . deserializeList (
typeRef ,
items . map ( ( row ) = > row . entity . value as Uint8Array ) ,
)
2022-02-10 16:32:47 +01:00
}
2022-06-16 17:23:48 +02:00
async getElementsOfType < T extends ElementEntity > ( typeRef : TypeRef < T > ) : Promise < Array < T > > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT entity from element_entities WHERE type = ${ getTypeId ( typeRef ) } `
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
return this . deserializeList (
typeRef ,
items . map ( ( row ) = > row . entity . value as Uint8Array ) ,
)
2022-06-16 17:23:48 +02:00
}
2022-08-11 16:38:53 +02:00
async getWholeList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id ) : Promise < Array < T > > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT entity FROM list_entities WHERE type = ${ getTypeId ( typeRef ) } AND listId = ${ listId } `
const items = ( await this . sqlCipherFacade . all ( query , params ) ) ? ? [ ]
return this . deserializeList (
typeRef ,
items . map ( ( row ) = > row . entity . value as Uint8Array ) ,
)
2022-02-10 16:32:47 +01:00
}
2022-08-11 16:38:53 +02:00
async dumpMetadata ( ) : Promise < Partial < OfflineDbMeta > > {
const query = "SELECT * from metadata"
2022-12-27 15:37:40 +01:00
const stored = ( await this . sqlCipherFacade . all ( query , [ ] ) ) . map ( ( row ) = > [ row . key . value as string , row . value . value as Uint8Array ] as const )
2022-08-11 16:38:53 +02:00
return Object . fromEntries ( stored . map ( ( [ key , value ] ) = > [ key , cborg . decode ( value ) ] ) ) as OfflineDbMeta
2022-01-12 14:43:01 +01:00
}
2022-10-21 15:53:39 +02:00
async setStoredModelVersion ( model : VersionMetadataBaseKey , version : number ) {
2022-08-11 16:38:53 +02:00
return this . putMetadata ( ` ${ model } -version ` , version )
}
getCustomCacheHandlerMap ( entityRestClient : EntityRestClient ) : CustomCacheHandlerMap {
if ( this . customCacheHandler == null ) {
2022-12-27 15:37:40 +01:00
this . customCacheHandler = new CustomCacheHandlerMap ( { ref : CalendarEventTypeRef , handler : new CustomCalendarEventCacheHandler ( entityRestClient ) } )
2022-08-11 16:38:53 +02:00
}
return this . customCacheHandler
}
2022-10-21 15:53:39 +02:00
getUserId ( ) : Id {
return assertNotNull ( this . userId , "No user id, not initialized?" )
}
async deleteAllOwnedBy ( owner : Id ) : Promise < void > {
{
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` DELETE FROM element_entities WHERE ownerGroup = ${ owner } `
2022-10-21 15:53:39 +02:00
await this . sqlCipherFacade . run ( query , params )
}
{
2022-11-02 11:37:37 +01:00
// first, check which list Ids contain entities owned by the lost group
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT listId, type FROM list_entities WHERE ownerGroup = ${ owner } `
2022-11-02 11:37:37 +01:00
const rangeRows = await this . sqlCipherFacade . all ( query , params )
2022-12-27 15:37:40 +01:00
const rows = rangeRows . map ( ( row ) = > untagSqlObject ( row ) as { listId : string ; type : string } )
const listIdsByType : Map < string , Set < Id > > = groupByAndMapUniquely (
rows ,
( row ) = > row . type ,
( row ) = > row . listId ,
)
2023-08-16 10:47:09 +02:00
// delete the ranges for those listIds
2022-11-02 11:37:37 +01:00
for ( const [ type , listIds ] of listIdsByType . entries ( ) ) {
2023-08-16 10:47:09 +02:00
// this particular query uses one other SQL var for the type.
const safeChunkSize = MAX_SAFE_SQL_VARS - 1
const listIdArr = Array . from ( listIds )
await this . runChunked ( safeChunkSize , listIdArr , ( c ) = > sql ` DELETE FROM ranges WHERE type = ${ type } AND listId IN ${ paramList ( c ) } ` )
await this . runChunked ( safeChunkSize , listIdArr , ( c ) = > sql ` DELETE FROM list_entities WHERE type = ${ type } AND listId IN ${ paramList ( c ) } ` )
2022-11-02 11:37:37 +01:00
}
2022-11-03 11:04:26 +01:00
}
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
{
const { query , params } = sql ` DELETE FROM blob_element_entities WHERE ownerGroup = ${ owner } `
await this . sqlCipherFacade . run ( query , params )
}
2022-11-03 11:04:26 +01:00
{
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` DELETE FROM lastUpdateBatchIdPerGroupId WHERE groupId = ${ owner } `
2022-11-03 11:04:26 +01:00
await this . sqlCipherFacade . run ( query , params )
2022-10-21 15:53:39 +02:00
}
}
2022-08-11 16:38:53 +02:00
private async putMetadata < K extends keyof OfflineDbMeta > ( key : K , value : OfflineDbMeta [ K ] ) : Promise < void > {
2023-02-07 10:18:22 +01:00
let encodedValue
try {
encodedValue = cborg . encode ( value )
} catch ( e ) {
console . log ( "[OfflineStorage] failed to encode metadata for key" , key , "with value" , value )
throw e
}
const { query , params } = sql ` INSERT OR REPLACE INTO metadata VALUES ( ${ key } , ${ encodedValue } ) `
2022-08-11 16:38:53 +02:00
await this . sqlCipherFacade . run ( query , params )
}
private async getMetadata < K extends keyof OfflineDbMeta > ( key : K ) : Promise < OfflineDbMeta [ K ] | null > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT value from metadata WHERE key = ${ key } `
2022-08-11 16:38:53 +02:00
const encoded = await this . sqlCipherFacade . get ( query , params )
return encoded && cborg . decode ( encoded . value . value as Uint8Array )
2022-01-12 14:43:01 +01:00
}
2022-02-10 16:32:47 +01:00
2022-05-06 14:39:31 +02:00
/ * *
2022-11-28 17:38:17 +01:00
* Clear out unneeded data from the offline database ( i . e . trash and spam lists , old data ) .
* This will be called after login ( CachePostLoginActions . ts ) to ensure fast login time .
* @param timeRangeDays : the maximum age of days that mails should be to be kept in the database . if null , will use a default value
* @param userId id of the current user . default , last stored userId
2022-05-06 14:39:31 +02:00
* /
2022-11-28 17:38:17 +01:00
async clearExcludedData ( timeRangeDays : number | null = this . timeRangeDays , userId : Id = this . getUserId ( ) ) : Promise < void > {
2022-08-11 16:38:53 +02:00
const user = await this . get ( UserTypeRef , null , userId )
2022-04-12 14:58:52 +02:00
2022-05-06 14:39:31 +02:00
// Free users always have default time range regardless of what is stored
const isFreeUser = user ? . accountType === AccountType . FREE
const timeRange = isFreeUser || timeRangeDays == null ? OFFLINE_STORAGE_DEFAULT_TIME_RANGE_DAYS : timeRangeDays
2022-05-24 18:35:24 +02:00
const cutoffTimestamp = this . dateProvider . now ( ) - timeRange * DAY_IN_MILLIS
2022-04-20 10:39:52 +02:00
const cutoffId = timestampToGeneratedId ( cutoffTimestamp )
2023-01-12 14:54:42 +01:00
const folders = await this . getListElementsOfType ( MailFolderTypeRef )
const folderSystem = new FolderSystem ( folders )
for ( const folder of folders ) {
2023-01-12 14:56:07 +01:00
if ( isSpamOrTrashFolder ( folderSystem , folder ) ) {
2022-04-20 10:39:52 +02:00
await this . deleteMailList ( folder . mails , GENERATED_MAX_ID )
} else {
await this . deleteMailList ( folder . mails , cutoffId )
}
}
2022-08-11 16:38:53 +02:00
}
private async createTables() {
for ( let [ name , definition ] of Object . entries ( TableDefinitions ) ) {
await this . sqlCipherFacade . run ( ` CREATE TABLE IF NOT EXISTS ${ name } ( ${ definition } ) ` , [ ] )
}
}
private async getRange ( type : string , listId : Id ) : Promise < Range | null > {
2022-12-27 15:37:40 +01:00
const { query , params } = sql ` SELECT upper, lower FROM ranges WHERE type = ${ type } AND listId = ${ listId } `
const row = ( await this . sqlCipherFacade . get ( query , params ) ) ? ? null
2022-08-11 16:38:53 +02:00
return mapNullable ( row , untagSqlObject ) as Range | null
2022-04-20 10:39:52 +02:00
}
/ * *
* This method deletes mails from { @param listId } what are older than { @param cutoffId } . as well as associated data
*
* For each mail we delete its body , headers , and all referenced attachments .
*
* When we delete the Files , we also delete the whole range for the user ' s File list . We need to delete the whole
* range because we only have one file list per mailbox , so if we delete something from the middle of it , the range
* will no longer be valid . ( this is future proofing , because as of now there is not going to be a Range set for the
* File list anyway , since we currently do not do range requests for Files .
*
* We do not delete ConversationEntries because :
* 1 . They are in the same list for the whole conversation so we can ' t adjust the range
* 2 . We might need them in the future for showing the whole thread
* /
private async deleteMailList ( listId : Id , cutoffId : Id ) : Promise < void > {
2022-11-30 17:15:08 +01:00
// We lock access to the "ranges" db here in order to prevent race conditions when accessing the "ranges" database.
await this . lockRangesDbAccess ( listId )
2022-12-12 17:51:21 +01:00
try {
// This must be done before deleting mails to know what the new range has to be
await this . updateRangeForList ( MailTypeRef , listId , cutoffId )
} finally {
// We unlock access to the "ranges" db here. We lock it in order to prevent race conditions when accessing the "ranges" database.
await this . unlockRangesDbAccess ( listId )
}
2022-04-20 10:39:52 +02:00
const mailsToDelete : IdTuple [ ] = [ ]
const headersToDelete : Id [ ] = [ ]
const attachmentsTodelete : IdTuple [ ] = [ ]
const mailbodiesToDelete : Id [ ] = [ ]
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const mailDetailsBlobToDelete : IdTuple [ ] = [ ]
const mailDetailsDraftToDelete : IdTuple [ ] = [ ]
2022-04-20 10:39:52 +02:00
const mails = await this . getWholeList ( MailTypeRef , listId )
for ( let mail of mails ) {
if ( firstBiggerThanSecond ( cutoffId , getElementId ( mail ) ) ) {
mailsToDelete . push ( mail . _id )
for ( const id of mail . attachments ) {
attachmentsTodelete . push ( id )
}
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
if ( isLegacyMail ( mail ) ) {
mailbodiesToDelete . push ( assertNotNull ( mail . body ) )
} else if ( isDetailsDraft ( mail ) ) {
const mailDetailsId = assertNotNull ( mail . mailDetailsDraft )
mailDetailsDraftToDelete . push ( mailDetailsId )
} else {
// mailDetailsBlob
const mailDetailsId = assertNotNull ( mail . mailDetails )
mailDetailsBlobToDelete . push ( mailDetailsId )
}
if ( mail . headers ) {
headersToDelete . push ( mail . headers )
}
2022-04-20 10:39:52 +02:00
}
}
2022-08-11 16:38:53 +02:00
await this . deleteIn ( MailBodyTypeRef , null , mailbodiesToDelete )
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
for ( let [ listId , elementIds ] of groupByAndMap ( mailDetailsBlobToDelete , listIdPart , elementIdPart ) . entries ( ) ) {
await this . deleteIn ( MailDetailsBlobTypeRef , listId , elementIds )
}
for ( let [ listId , elementIds ] of groupByAndMap ( mailDetailsDraftToDelete , listIdPart , elementIdPart ) . entries ( ) ) {
await this . deleteIn ( MailDetailsDraftTypeRef , listId , elementIds )
}
2022-08-11 16:38:53 +02:00
await this . deleteIn ( MailHeadersTypeRef , null , headersToDelete )
2022-04-20 10:39:52 +02:00
for ( let [ listId , elementIds ] of groupByAndMap ( attachmentsTodelete , listIdPart , elementIdPart ) . entries ( ) ) {
2022-08-11 16:38:53 +02:00
await this . deleteIn ( FileTypeRef , listId , elementIds )
await this . deleteRange ( FileTypeRef , listId )
2022-04-20 10:39:52 +02:00
}
2022-08-11 16:38:53 +02:00
await this . deleteIn ( MailTypeRef , listId , mailsToDelete . map ( elementIdPart ) )
}
private async deleteIn ( typeRef : TypeRef < unknown > , listId : Id | null , elementIds : Id [ ] ) : Promise < void > {
2023-09-18 12:07:46 +02:00
if ( elementIds . length === 0 ) return
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
const typeModel = await resolveTypeReference ( typeRef )
switch ( typeModel . type ) {
2023-01-12 16:48:28 +01:00
case TypeId . Element :
2023-08-16 10:47:09 +02:00
return await this . runChunked (
MAX_SAFE_SQL_VARS - 1 ,
elementIds ,
( c ) = > sql ` DELETE FROM element_entities WHERE type = ${ getTypeId ( typeRef ) } AND elementId IN ${ paramList ( c ) } ` ,
)
2023-01-12 16:48:28 +01:00
case TypeId . ListElement :
2023-08-16 10:47:09 +02:00
return await this . runChunked (
MAX_SAFE_SQL_VARS - 2 ,
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
elementIds ,
2023-08-16 10:47:09 +02:00
( c ) = > sql ` DELETE FROM list_entities WHERE type = ${ getTypeId ( typeRef ) } AND listId = ${ listId } AND elementId IN ${ paramList ( c ) } ` ,
)
2023-01-12 16:48:28 +01:00
case TypeId . BlobElement :
2023-08-16 10:47:09 +02:00
return await this . runChunked (
MAX_SAFE_SQL_VARS - 2 ,
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
elementIds ,
2023-08-16 10:47:09 +02:00
( c ) = > sql ` DELETE FROM blob_element_entities WHERE type = ${ getTypeId ( typeRef ) } AND listId = ${ listId } AND elementId IN ${ paramList ( c ) } ` ,
)
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
default :
throw new Error ( "must be a persistent type" )
}
2022-04-20 10:39:52 +02:00
}
2022-11-30 17:15:08 +01:00
/ * *
* We want to lock the access to the "ranges" db when updating / reading the
* offline available mail list ranges for each mail list ( referenced using the listId ) .
* @param listId the mail list that we want to lock
* /
async lockRangesDbAccess ( listId : Id ) {
await this . sqlCipherFacade . lockRangesDbAccess ( listId )
}
/ * *
* This is the counterpart to the function "lockRangesDbAccess(listId)" .
* @param listId the mail list that we want to unlock
* /
async unlockRangesDbAccess ( listId : Id ) {
await this . sqlCipherFacade . unlockRangesDbAccess ( listId )
}
2022-04-20 10:39:52 +02:00
private async updateRangeForList < T extends ListElementEntity > ( typeRef : TypeRef < T > , listId : Id , cutoffId : Id ) : Promise < void > {
2022-05-23 17:09:09 +02:00
const type = getTypeId ( typeRef )
2022-04-20 10:39:52 +02:00
2022-08-11 16:38:53 +02:00
const range = await this . getRange ( type , listId )
2022-04-20 10:39:52 +02:00
if ( range == null ) {
return
}
// If the range for a given list is complete from the beginning (starts at GENERATED_MIN_ID), then we only want to actually modify the
// saved range if we would be removing elements from the list, in order to not lose the information that the range is complete in storage.
// So we have to check how old the oldest element in said range is. If it is newer than cutoffId, then we will not modify the range,
// otherwise we will just modify it normally
if ( range . lower === GENERATED_MIN_ID ) {
const entities = await this . provideFromRange ( typeRef , listId , GENERATED_MIN_ID , 1 , false )
const id = mapNullable ( entities [ 0 ] , getElementId )
const rangeWontBeModified = id == null || firstBiggerThanSecond ( id , cutoffId ) || id === cutoffId
if ( rangeWontBeModified ) {
return
}
}
if ( firstBiggerThanSecond ( cutoffId , range . lower ) ) {
// If the upper id of the range is below the cutoff, then the entire range will be deleted from the storage
// so we just delete the range as well
// Otherwise, we only want to modify
if ( firstBiggerThanSecond ( cutoffId , range . upper ) ) {
2022-08-11 16:38:53 +02:00
await this . deleteRange ( typeRef , listId )
2022-04-20 10:39:52 +02:00
} else {
2022-08-11 16:38:53 +02:00
await this . setLowerRangeForList ( typeRef , listId , cutoffId )
2022-04-20 10:39:52 +02:00
}
}
}
2022-08-11 16:38:53 +02:00
private serialize ( originalEntity : SomeEntity ) : Uint8Array {
2023-02-07 10:18:22 +01:00
try {
return cborg . encode ( originalEntity , { typeEncoders : customTypeEncoders } )
} catch ( e ) {
console . log ( "[OfflineStorage] failed to encode entity of type" , originalEntity . _type , "with id" , originalEntity . _id )
throw e
}
2022-04-20 10:39:52 +02:00
}
2022-08-11 16:38:53 +02:00
private deserialize < T extends SomeEntity > ( typeRef : TypeRef < T > , loaded : Uint8Array ) : T {
2022-12-27 15:37:40 +01:00
const deserialized = cborg . decode ( loaded , { tags : customTypeDecoders } )
2022-08-11 16:38:53 +02:00
// TypeRef cannot be deserialized back automatically. We could write a codec for it but we don't actually
// need to store it so we just "patch" it.
// Some places rely on TypeRef being a class and not a plain object.
deserialized . _type = typeRef
return deserialized
2022-04-20 10:39:52 +02:00
}
2022-05-17 17:40:44 +02:00
2022-08-11 16:38:53 +02:00
private deserializeList < T extends SomeEntity > ( typeRef : TypeRef < T > , loaded : Array < Uint8Array > ) : Array < T > {
2022-12-27 15:37:40 +01:00
return loaded . map ( ( entity ) = > this . deserialize ( typeRef , entity ) )
2022-05-17 17:40:44 +02:00
}
2023-08-16 10:47:09 +02:00
/ * *
* convenience method to run a potentially too large query over several chunks .
* chunkSize must be chosen such that the total number of SQL variables in the final query does not exceed MAX_SAFE_SQL_VARS
* * /
private async runChunked ( chunkSize : number , originalList : SqlValue [ ] , formatter : ( chunk : SqlValue [ ] ) = > FormattedQuery ) : Promise < void > {
for ( const chunk of splitInChunks ( chunkSize , originalList ) ) {
const formattedQuery = formatter ( chunk )
await this . sqlCipherFacade . run ( formattedQuery . query , formattedQuery . params )
}
}
2022-08-11 16:38:53 +02:00
}
2022-07-04 14:55:17 +02:00
2022-08-11 16:38:53 +02:00
/ *
2023-08-16 10:47:09 +02:00
* used to automatically create the right amount of SQL variables for selecting ids from a dynamic list .
* must be used within sql ` <query> ` template string to inline the logic into the query .
*
* It is very important that params is kept to a size such that the total amount of SQL variables is
* less than MAX_SAFE_SQL_VARS .
2022-08-11 16:38:53 +02:00
* /
function paramList ( params : SqlValue [ ] ) : SqlFragment {
2022-12-27 15:37:40 +01:00
const qs = params . map ( ( ) = > "?" ) . join ( "," )
2022-08-11 16:38:53 +02:00
return new SqlFragment ( ` ( ${ qs } ) ` , params )
}
/ * *
2022-08-15 14:22:44 +02:00
* comparison to select ids that are bigger or smaller than a parameter id
2023-08-16 10:47:09 +02:00
* must be used within sql ` <query> ` template string to inline the logic into the query .
*
* will always insert 3 constants and 3 SQL variables into the query .
2022-08-11 16:38:53 +02:00
* /
function firstIdBigger ( . . . args : [ string , "elementId" ] | [ "elementId" , string ] ) : SqlFragment {
let [ l , r ] : [ string , string ] = args
let v
if ( l === "elementId" ) {
v = r
r = "?"
} else {
v = l
l = "?"
}
2022-12-27 15:37:40 +01:00
return new SqlFragment ( ` (CASE WHEN length( ${ l } ) > length( ${ r } ) THEN 1 WHEN length( ${ l } ) < length( ${ r } ) THEN 0 ELSE ${ l } > ${ r } END) ` , [ v , v , v ] )
2022-08-11 16:38:53 +02:00
}
/ * *
2022-08-15 14:22:44 +02:00
* this tagged template function exists because android doesn ' t allow us to define SQL functions , so we have made a way to inline
* SQL fragments into queries .
* to make it less error - prone , we automate the generation of the params array for the actual sql call .
* In this way , we offload the escaping of actual user content to the SQL engine , which makes this safe from an SQLI point of view .
2022-08-11 16:38:53 +02:00
*
* usage example :
* const type = "sys/User"
* const listId = "someList"
* const startId = "ABC"
2022-08-15 14:22:44 +02:00
* sql ` SELECT entity FROM list_entities WHERE type = ${ type } AND listId = ${ listId } AND ${ firstIdBigger ( startId , "elementId" ) } `
*
* this will result in
* const { query , params } = {
* query : ` SELECT entity FROM list_entities WHERE type = ? AND listId = ? AND (CASE WHEN length(?) > length(elementId) THEN 1 WHEN length(?) < length(elementId) THEN 0 ELSE ? > elementId END) ` ,
* params : [
* { type : SqlType . String , value : "sys/User" } ,
* { type : SqlType . String , value : "someList" } ,
* { type : SqlType . String , value : "ABC" } ,
* { type : SqlType . String , value : "ABC" } ,
* { type : SqlType . String , value : "ABC" }
* ]
* }
2022-08-11 16:38:53 +02:00
*
2023-08-16 10:47:09 +02:00
* which can be consumed by sql . run ( query , params ) .
*
* It is important that the caller ensures that the amount of SQL variables does not exceed MAX_SAFE_SQL_VARS !
* Violating this rule will lead to an uncaught error with bad stack traces .
2022-08-11 16:38:53 +02:00
* /
2023-08-15 16:43:50 +02:00
export function sql ( queryParts : TemplateStringsArray , . . . paramInstances : ( SqlValue | SqlFragment ) [ ] ) : FormattedQuery {
2022-08-11 16:38:53 +02:00
let query = ""
let params : TaggedSqlValue [ ] = [ ]
2023-08-15 16:43:50 +02:00
let i : number
2022-08-11 16:38:53 +02:00
for ( i = 0 ; i < paramInstances . length ; i ++ ) {
query += queryParts [ i ]
const param = paramInstances [ i ]
if ( param instanceof SqlFragment ) {
query += param . text
params . push ( . . . param . params . map ( tagSqlValue ) )
} else {
query += "?"
params . push ( tagSqlValue ( param ) )
2022-07-04 14:55:17 +02:00
}
}
2022-08-11 16:38:53 +02:00
query += queryParts [ i ]
2022-12-27 15:37:40 +01:00
return { query , params }
2022-04-20 10:39:52 +02:00
}
2022-08-11 16:38:53 +02:00
class SqlFragment {
2022-12-27 15:37:40 +01:00
constructor ( readonly text : string , readonly params : SqlValue [ ] ) { }
}