Kinvey/swift-sdk

View on GitHub
Kinvey/Kinvey/Migration.swift

Summary

Maintainability
A
2 hrs
Test Coverage
//
//  Migration.swift
//  Kinvey
//
//  Created by Victor Barros on 2016-03-22.
//  Copyright © 2016 Kinvey. All rights reserved.
//

import Foundation
import Realm
import RealmSwift

public typealias Schema = (version: CUnsignedLongLong, migrationHandler: Migration.MigrationHandler?)

/// Class used to perform migrations in your local cache.
open class Migration: NSObject {
    
    public typealias MigrationHandler = (Migration, UInt64) -> Void
    public typealias MigrationObjectHandler = (JsonDictionary) -> JsonDictionary?
    
    let realmMigration: RealmSwift.Migration
    
    init(realmMigration: RealmSwift.Migration) {
        self.realmMigration = realmMigration
    }
    
    private class func buildRealmBaseConfiguration(
        encryptionKey: Data?,
        schemaVersion: CUnsignedLongLong,
        migrationHandler: Migration.MigrationHandler?,
        compactCacheOnLaunch: Bool
    ) -> Realm.Configuration {
        var realmBaseConfiguration = Realm.Configuration.defaultConfiguration //copy
        if let encryptionKey = encryptionKey {
            realmBaseConfiguration.encryptionKey = encryptionKey
        }
        realmBaseConfiguration.schemaVersion = schemaVersion
        if let migrationHandler = migrationHandler {
            realmBaseConfiguration.migrationBlock = { migration, oldSchemaVersion in
                let migration = Migration(realmMigration: migration)
                migrationHandler(migration, oldSchemaVersion)
            }
        } else {
            realmBaseConfiguration.deleteRealmIfMigrationNeeded = true
        }
        realmBaseConfiguration.shouldCompactOnLaunch = { totalBytes, usedBytes in
            log.debug("Cache: \(usedBytes) bytes used in a total of \(totalBytes) bytes. Compact Cache on Launch: \(compactCacheOnLaunch)")
            return compactCacheOnLaunch
        }
        return realmBaseConfiguration
    }
    
    internal class func performMigration(
        persistenceId: String,
        encryptionKey: Data? = nil,
        schemaVersion: CUnsignedLongLong = 0,
        migrationHandler: Migration.MigrationHandler? = nil,
        compactCacheOnLaunch: Bool = true
    ) throws {
        let realmBaseConfiguration = buildRealmBaseConfiguration(
            encryptionKey: encryptionKey,
            schemaVersion: schemaVersion,
            migrationHandler: migrationHandler,
            compactCacheOnLaunch: compactCacheOnLaunch
        )
        let baseFolderURL = Client.fileURL(appKey: persistenceId).deletingLastPathComponent()
        let fileManager = FileManager.default
        if let allFilesURL = try? fileManager.contentsOfDirectory(at: baseFolderURL, includingPropertiesForKeys: nil) {
            for realmFileURL in allFilesURL.filter({ $0.lastPathComponent.hasSuffix(".realm") }) {
                var realmConfiguration = realmBaseConfiguration //copy
                realmConfiguration.fileURL = realmFileURL
                try performMigration(realmConfiguration: realmConfiguration)
            }
        }
    }
    
    private class func performMigration(realmConfiguration: Realm.Configuration) throws {
        var realmConfiguration = realmConfiguration //copy
        do {
            try Realm.performMigration(for: realmConfiguration)
        } catch {
            log.error("Database migration failed: deleting local database.\nDetails of the failure: \(error)")
            realmConfiguration.deleteRealmIfMigrationNeeded = true
            try Realm.performMigration(for: realmConfiguration)
        }
    }
    
    /// Method that performs a migration in a specific collection.
    open func execute<T: Entity>(_ type: T.Type, oldClassName: String? = nil, migrationObjectHandler: MigrationObjectHandler? = nil) {
        let className = type.className()
        let oldSchemaClassName = oldClassName ?? className
        let oldObjectSchema = realmMigration.oldSchema[oldSchemaClassName]
        if let oldObjectSchema = oldObjectSchema {
            let oldProperties = oldObjectSchema.properties.map { $0.name }
            realmMigration.enumerateObjects(ofType: oldSchemaClassName) { (oldObject, newObject) in
                if let oldObject = oldObject, let newObject = newObject {
                    let oldDictionary = oldObject.dictionaryWithValues(forKeys: oldProperties)
                    
                    if var newDictionary = migrationObjectHandler?(oldDictionary) {
                        if let primaryKeyProperty = oldObjectSchema.primaryKeyProperty {
                            newDictionary.removeValue(forKey: primaryKeyProperty.name)
                        }
                        newObject.setValuesForKeys(newDictionary)
                    } else {
                        self.realmMigration.delete(newObject)
                    }
                }
            }
        }
    }
    
}