Kinvey/swift-sdk

View on GitHub
Kinvey/Kinvey/DataStore.swift

Summary

Maintainability
F
2 wks
Test Coverage
//
//  BaseStore.swift
//  Kinvey
//
//  Created by Victor Barros on 2015-12-14.
//  Copyright © 2015 Kinvey. All rights reserved.
//

import Foundation
import PromiseKit

/// Class to interact with a specific collection in the backend.
open class DataStore<T: Persistable> where T: NSObject {
    
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use Result<AnyRandomAccessCollection<T>, Swift.Error> instead")
    public typealias ArrayCompletionHandler = ([T]?, Swift.Error?) -> Void
    
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use Result<T, Swift.Error> instead")
    public typealias ObjectCompletionHandler = (T?, Swift.Error?) -> Void
    
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use Result<Int, Swift.Error> instead")
    public typealias IntCompletionHandler = (Int?, Swift.Error?) -> Void
    
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use Result<UInt, [Swift.Error]> instead")
    public typealias UIntErrorTypeArrayCompletionHandler = (UInt?, [Swift.Error]?) -> Void
    
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use Result<(Int, AnyRandomAccessCollection<T>), [Swift.Error]> instead")
    public typealias UIntArrayCompletionHandler = (UInt?, [T]?, [Swift.Error]?) -> Void
    
    fileprivate let readPolicy: ReadPolicy
    fileprivate let writePolicy: WritePolicy
    
    /// Collection name that matches with the name in the backend.
    public let collectionName: String
    
    /// Client instance attached to the DataStore.
    public let client: Client
    
    /// DataStoreType defines how the DataStore will behave.
    public let type: StoreType
    
    public let validationStrategy: ValidationStrategy?
    
    fileprivate let fileURL: URL?
    
    internal let cache: AnyCache<T>?
    internal let sync: AnySync?
    
    @available(*, deprecated, message: "Deprecated in version 3.18.2. Please use DataStore.options instead")
    open var deltaSet: Bool {
        return options?.deltaSet ?? false
    }
    
    open var options: Options?
    
    fileprivate let uuid = UUID()
    
    private let autoPagination: Bool
    
    /// TTL (Time to Live) defines a filter of how old the data returned from the DataStore can be.
    open var ttl: TTL? {
        didSet {
            if let cache = cache {
                if let (value, unit) = ttl {
                    cache.ttl = unit.toTimeInterval(value)
                } else {
                    cache.ttl = nil
                }
            }
        }
    }
    
    /**
     Factory method that returns a `DataStore`.
     - parameter type: defines the data store type which will define the behavior of the `DataStore`. Default value: `.cache`
     - parameter deltaSet: Enables delta set cache which will increase performance and reduce data consumption. Default value: `false`
     - parameter client: define the `Client` to be used for all the requests for the `DataStore` that will be returned. Default value: `Kinvey.sharedClient`
     - parameter tag: A tag/nickname for your `DataStore` which will cache instances with the same tag name. Default value: `Kinvey.defaultTag`
     - parameter validationStrategy: (Optional) Defines a strategy to validate results upfront. Default value: `nil`
     - returns: An instance of `DataStore` which can be a new instance or a cached instance if you are passing a `tag` parameter.
     */
    @available(*, deprecated, message: "Deprecated in version 3.21.0. Please use `collection(type:autoPagination:tag:validationStrategy:options:)` instead")
    open class func collection(
        _ type: StoreType = .cache,
        autoPagination: Bool = false,
        tag: String = defaultTag,
        validationStrategy: ValidationStrategy? = nil,
        options: Options? = nil
    ) throws -> DataStore {
        return try collection(
            type: type,
            autoPagination: autoPagination,
            tag: tag,
            validationStrategy: validationStrategy,
            options: options
        )
    }
    
    /**
     Factory method that returns a `DataStore`.
     - parameter type: defines the data store type which will define the behavior of the `DataStore`. Default value: `.auto`
     - parameter deltaSet: Enables delta set cache which will increase performance and reduce data consumption. Default value: `false`
     - parameter client: define the `Client` to be used for all the requests for the `DataStore` that will be returned. Default value: `Kinvey.sharedClient`
     - parameter tag: A tag/nickname for your `DataStore` which will cache instances with the same tag name. Default value: `Kinvey.defaultTag`
     - parameter validationStrategy: (Optional) Defines a strategy to validate results upfront. Default value: `nil`
     - returns: An instance of `DataStore` which can be a new instance or a cached instance if you are passing a `tag` parameter.
     */
    open class func collection(
        type: StoreType,
        autoPagination: Bool = false,
        tag: String = defaultTag,
        validationStrategy: ValidationStrategy? = nil,
        options: Options? = nil
    ) throws -> DataStore {
        let client = options?.client ?? sharedClient
        if !client.isInitialized() {
            throw Error.invalidOperation(description: "Client is not initialized. Call Kinvey.sharedClient.initialize(...) to initialize the client before creating a DataStore.")
        }
        let fileURL = client.fileURL(tag)
        let dataStore = try DataStore<T>(
            type: type,
            autoPagination: autoPagination,
            client: client,
            fileURL: fileURL,
            encryptionKey: client.encryptionKey,
            validationStrategy: validationStrategy,
            options: options
        )
        return dataStore
    }
    
    /**
     Factory method that returns a new instance of a DataStore copying all the
     current configuration but for a new type.
     - parameter newType: Type for the new DataStore instance
     - returns: A new DataStore instance for the type specified
     */
    open func collection<NewType: Persistable>(
        newType: NewType.Type
    ) throws -> DataStore<NewType> where NewType: NSObject {
        return try DataStore<NewType>(
            type: type,
            autoPagination: autoPagination,
            client: client,
            fileURL: fileURL,
            encryptionKey: client.encryptionKey,
            validationStrategy: validationStrategy,
            options: options
        )
    }
    
    fileprivate init(
        type: StoreType,
        autoPagination: Bool,
        client: Client,
        fileURL: URL?,
        encryptionKey: Data?,
        validationStrategy: ValidationStrategy?,
        options: Options?
    ) throws {
        self.type = type
        self.autoPagination = autoPagination
        self.client = client
        self.fileURL = fileURL
        collectionName = try! T.collectionName()
        if type != .network, let _ = T.self as? Entity.Type {
            cache = try client.cacheManager.cache(fileURL: fileURL, type: T.self)
            sync = try client.syncManager.sync(fileURL: fileURL, type: T.self)
        } else {
            cache = nil
            sync = nil
        }
        readPolicy = type.readPolicy
        writePolicy = type.writePolicy
        self.validationStrategy = validationStrategy
        self.options = options
    }
    
    private func validate(id: String) throws {
        if id.isEmpty {
            throw Error.invalidOperation(description: "id cannot be an empty string")
        }
    }
    
    /**
     Gets a single record using the `_id` of the record.
     
     - parameter id: The `_id` value of the entity to be find
     - parameter completionHandler: Completion handler to be called once the respose returns
     - returns: A `AnyRequest` instance which will allow cancel the request later
     */
    @discardableResult
    open func find(
        _ id: String,
        options: Options? = nil,
        completionHandler: ((Swift.Result<T, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<T, Swift.Error>> {
        do {
            try validate(id: id)
        } catch {
            return errorRequest(error: error, completionHandler: completionHandler)
        }
        
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let operation = GetOperation<T>(
            id: id,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let request = operation.execute { result in
            if let completionHandler = completionHandler {
                DispatchQueue.main.async {
                    completionHandler(result)
                }
            }
        }
        return request
    }
    
    /**
     Gets a list of records that matches with the query passed by parameter.
     - parameter query: The query used to filter the results
     - parameter deltaSet: Enforces delta set cache otherwise use the client's `deltaSet` value. Default value: `false`
     - parameter readPolicy: Enforces a different `ReadPolicy` otherwise use the client's `ReadPolicy`. Default value: `nil`
     - parameter completionHandler: Completion handler to be called once the respose returns
     - returns: A `Request` instance which will allow cancel the request later
     */
    @discardableResult
    open func find(
        _ query: Query = Query(),
        options: Options? = nil,
        completionHandler: ((Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>> {
        do {
            let readPolicy = options?.readPolicy ?? self.readPolicy
            let operation = FindOperation<T>(
                query: Query(query: query, persistableType: T.self),
                autoPagination: autoPagination,
                readPolicy: readPolicy,
                validationStrategy: validationStrategy,
                cache: cache,
                options: try Options(specific: options, general: self.options)
            )
            let request = operation.execute { result in
                completionHandler?(result)
            }
            return request
        } catch {
            return errorRequest(error: error, completionHandler: completionHandler)
        }
    }
    
    /**
     Gets a count of how many records that matches with the (optional) query passed by parameter.
     - parameter query: The query used to filter the results
     - parameter readPolicy: Enforces a different `ReadPolicy` otherwise use the client's `ReadPolicy`. Default value: `nil`
     - parameter completionHandler: Completion handler to be called once the respose returns
     - returns: A `Request` instance which will allow cancel the request later
     */
    @discardableResult
    open func count(
        _ query: Query? = nil,
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let operation = CountOperation<T>(
            query: Query(query: query ?? Query(), persistableType: T.self),
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let request = operation.execute { result in
            DispatchQueue.main.async {
                completionHandler?(result)
            }
        }
        return AnyRequest(request)
    }
    
    @discardableResult
    open func group(
        keys: [String]? = nil,
        initialObject: JsonDictionary,
        reduceJSFunction: String,
        condition: NSPredicate? = nil,
        options: Options? = nil,
        completionHandler: ((Swift.Result<[AggregationCustomResult<T>], Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<[AggregationCustomResult<T>], Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let client = options?.client ?? self.client
        let keys = keys ?? []
        let aggregation: Aggregation = .custom(
            keys: keys,
            initialObject: initialObject,
            reduceJSFunction: reduceJSFunction
        )
        let operation = AggregateOperation<T>(
            aggregation: aggregation,
            condition: condition,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let convert = { (results: [JsonDictionary]) -> [AggregationCustomResult<T>] in
            let array = results.map { (json) -> AggregationCustomResult<T> in
                var json = json
                json[Entity.EntityCodingKeys.entityId] = groupId
                return AggregationCustomResult<T>(
                    value: try! client.jsonParser.parseObject(T.self, from: json),
                    custom: json
                )
            }
            return array
        }
        let request = operation.execute {
            let result: Swift.Result<[AggregationCustomResult<T>], Swift.Error>
            switch $0 {
            case .success(let results):
                result = .success(convert(results))
            case .failure(let error):
                result = .failure(error)
            }
            if let completionHandler = completionHandler {
                DispatchQueue.main.async {
                    completionHandler(result)
                }
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success(let results):
                    return .success(convert(results))
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    @discardableResult
    open func group<Count: CountType>(
        count keys: [String],
        countType: Count.Type? = nil,
        condition: NSPredicate? = nil,
        options: Options? = nil,
        completionHandler: @escaping (Swift.Result<[AggregationCountResult<T, Count>], Swift.Error>) -> Void
    ) -> AnyRequest<Swift.Result<[AggregationCountResult<T, Count>], Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let client = options?.client ?? self.client
        let aggregation: Aggregation = .count(keys: keys)
        let operation = AggregateOperation<T>(
            aggregation: aggregation,
            condition: condition,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let convert = { (results: [JsonDictionary]) -> [AggregationCountResult<T, Count>] in
            let array = try results.map { (json) -> AggregationCountResult<T, Count> in
                var json = json
                json[Entity.EntityCodingKeys.entityId] = groupId
                return AggregationCountResult<T, Count>(
                    value: try! client.jsonParser.parseObject(T.self, from: json),
                    count: json[try aggregation.resultKey()] as! Count
                )
            }
            return array
        }
        let request = operation.execute { result in
            switch result {
            case .success(let results):
                DispatchQueue.main.async {
                    let result: Swift.Result<[AggregationCountResult<T, Count>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    completionHandler(result)
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    completionHandler(.failure(error))
                }
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success(let results):
                    let result: Swift.Result<[AggregationCountResult<T, Count>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    return result
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    @discardableResult
    open func group<Sum: AddableType>(
        keys: [String],
        sum: String,
        sumType: Sum.Type? = nil,
        condition: NSPredicate? = nil,
        options: Options? = nil,
        completionHandler: @escaping (Swift.Result<[AggregationSumResult<T, Sum>], Swift.Error>) -> Void
    ) -> AnyRequest<Swift.Result<[AggregationSumResult<T, Sum>], Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let client = options?.client ?? self.client
        let aggregation: Aggregation = .sum(keys: keys, sum: sum)
        let operation = AggregateOperation<T>(
            aggregation: aggregation,
            condition: condition,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let convert = { (results: [JsonDictionary]) -> [AggregationSumResult<T, Sum>] in
            let array = try results.map { (json) -> AggregationSumResult<T, Sum> in
                var json = json
                json[Entity.EntityCodingKeys.entityId] = groupId
                return AggregationSumResult<T, Sum>(
                    value: try! client.jsonParser.parseObject(T.self, from: json),
                    sum: json[try aggregation.resultKey()] as! Sum
                )
            }
            return array
        }
        let request = operation.execute { result in
            switch result {
            case .success(let results):
                DispatchQueue.main.async {
                    let result: Swift.Result<[AggregationSumResult<T, Sum>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    completionHandler(result)
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    completionHandler(.failure(error))
                }
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success(let results):
                    let result: Swift.Result<[AggregationSumResult<T, Sum>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    return result
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    @discardableResult
    open func group<Avg: AddableType>(
        keys: [String],
        avg: String,
        avgType: Avg.Type? = nil,
        condition: NSPredicate? = nil,
        options: Options? = nil,
        completionHandler: @escaping (Swift.Result<[AggregationAvgResult<T, Avg>], Swift.Error>) -> Void
    ) -> AnyRequest<Swift.Result<[AggregationAvgResult<T, Avg>], Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let client = options?.client ?? self.client
        let aggregation: Aggregation = .avg(keys: keys, avg: avg)
        let operation = AggregateOperation<T>(
            aggregation: aggregation,
            condition: condition,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let convert = { (results: [JsonDictionary]) -> [AggregationAvgResult<T, Avg>] in
            let array = try results.map { (json) -> AggregationAvgResult<T, Avg> in
                var json = json
                json[Entity.EntityCodingKeys.entityId] = groupId
                return AggregationAvgResult<T, Avg>(
                    value: try! client.jsonParser.parseObject(T.self, from: json),
                    avg: json[try aggregation.resultKey()] as! Avg
                )
            }
            return array
        }
        let request = operation.execute { result in
            switch result {
            case .success(let results):
                DispatchQueue.main.async {
                    let result: Swift.Result<[AggregationAvgResult<T, Avg>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    completionHandler(result)
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    completionHandler(.failure(error))
                }
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success(let results):
                    let result: Swift.Result<[AggregationAvgResult<T, Avg>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    return result
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    @discardableResult
    open func group<Min: MinMaxType>(
        keys: [String],
        min: String,
        minType: Min.Type? = nil,
        condition: NSPredicate? = nil,
        options: Options? = nil,
        completionHandler: @escaping (Swift.Result<[AggregationMinResult<T, Min>], Swift.Error>) -> Void
    ) -> AnyRequest<Swift.Result<[AggregationMinResult<T, Min>], Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let client = options?.client ?? self.client
        let aggregation: Aggregation = .min(keys: keys, min: min)
        let operation = AggregateOperation<T>(
            aggregation: aggregation,
            condition: condition,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let convert = { (results: [JsonDictionary]) -> [AggregationMinResult<T, Min>] in
            let array = try results.map { (json) -> AggregationMinResult<T, Min> in
                var json = json
                json[Entity.EntityCodingKeys.entityId] = groupId
                return AggregationMinResult<T, Min>(
                    value: try! client.jsonParser.parseObject(T.self, from: json),
                    min: json[try aggregation.resultKey()] as! Min
                )
            }
            return array
        }
        let request = operation.execute { result in
            switch result {
            case .success(let results):
                DispatchQueue.main.async {
                    let result: Swift.Result<[AggregationMinResult<T, Min>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    completionHandler(result)
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    completionHandler(.failure(error))
                }
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success(let results):
                    let result: Swift.Result<[AggregationMinResult<T, Min>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    return result
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    @discardableResult
    open func group<Max: MinMaxType>(
        keys: [String],
        max: String,
        maxType: Max.Type? = nil,
        condition: NSPredicate? = nil,
        options: Options? = nil,
        completionHandler: @escaping (Swift.Result<[AggregationMaxResult<T, Max>], Swift.Error>) -> Void
    ) -> AnyRequest<Swift.Result<[AggregationMaxResult<T, Max>], Swift.Error>> {
        let readPolicy = options?.readPolicy ?? self.readPolicy
        let client = options?.client ?? self.client
        let aggregation: Aggregation = .max(keys: keys, max: max)
        let operation = AggregateOperation<T>(
            aggregation: aggregation,
            condition: condition,
            readPolicy: readPolicy,
            validationStrategy: validationStrategy,
            cache: cache,
            options: options
        )
        let convert = { (results: [JsonDictionary]) -> [AggregationMaxResult<T, Max>] in
            let array = try results.map { (json) -> AggregationMaxResult<T, Max> in
                var json = json
                json[Entity.EntityCodingKeys.entityId] = groupId
                return AggregationMaxResult<T, Max>(
                    value: try! client.jsonParser.parseObject(T.self, from: json),
                    max: json[try aggregation.resultKey()] as! Max
                )
            }
            return array
        }
        let request = operation.execute { result in
            switch result {
            case .success(let results):
                DispatchQueue.main.async {
                    let result: Swift.Result<[AggregationMaxResult<T, Max>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    completionHandler(result)
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    completionHandler(.failure(error))
                }
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success(let results):
                    let result: Swift.Result<[AggregationMaxResult<T, Max>], Swift.Error>
                    do {
                        result = .success(try convert(results))
                    } catch {
                        result = .failure(error)
                    }
                    return result
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    /// Creates or updates a record.
    @discardableResult
    open func save(
        _ persistable: T,
        options: Options? = nil,
        completionHandler: ((Swift.Result<T, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<T, Swift.Error>> {
        let writePolicy = options?.writePolicy ?? self.writePolicy
        let operation = SaveOperation<T>(
            persistable: persistable,
            writePolicy: writePolicy,
            sync: sync,
            cache: cache,
            options: options
        )
        let request = operation.execute { result in
            DispatchQueue.main.async {
                completionHandler?(result)
            }
        }
        return request
    }
    
    /// Creates or updates a record.
    @discardableResult
    open func save<C: RandomAccessCollection>(
        _ persistable: C,
        options: Options? = nil,
        completionHandler: ((Swift.Result<MultiSaveResultTuple<T>, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<MultiSaveResultTuple<T>, Swift.Error>> where C.Element == T {
        let writePolicy = options?.writePolicy ?? self.writePolicy
        let operation = SaveMultiOperation<T>(
            persistable: persistable,
            writePolicy: writePolicy,
            sync: sync,
            cache: cache,
            options: options
        )
        let request = operation.execute { result in
            DispatchQueue.main.async {
                completionHandler?(result)
            }
        }
        return request
    }
    
    /// Deletes a record.
    @discardableResult
    open func remove(
        _ persistable: T,
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)? = nil
    ) throws -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        guard let id = persistable.entityId else {
            log.error("Object Id is missing")
            throw Error.objectIdMissing
        }
        return remove(
            byId: id,
            options: options,
            completionHandler: completionHandler
        )
    }
    
    /// Deletes a list of records.
    @discardableResult
    open func remove<S: Sequence>(
        _ array: S,
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)?
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> where S.Element == T {
        let ids = array.compactMap { $0.entityId }
        return remove(
            byIds: ids,
            options: options,
            completionHandler: completionHandler
        )
    }
    
    /// Deletes a record using the `_id` of the record.
    @discardableResult
    open func remove(
        byId id: String,
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        do {
            try validate(id: id)
        } catch {
            return errorRequest(error: error, completionHandler: completionHandler)
        }

        let writePolicy = options?.writePolicy ?? self.writePolicy
        let operation = RemoveByIdOperation<T>(
            objectId: id,
            writePolicy: writePolicy,
            sync: sync,
            cache: cache,
            options: options
        )
        let request = operation.execute { result in
            if let completionHandler = completionHandler {
                DispatchQueue.main.async {
                    completionHandler(result)
                }
            }
        }
        return request
    }
    
    /// Deletes a list of records using the `_id` of the records.
    @discardableResult
    open func remove(
        byIds ids: [String],
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)?
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        guard !ids.isEmpty else {
            return errorRequest(error: Error.invalidOperation(description: "ids cannot be an empty array"), completionHandler: completionHandler)
        }
        
        let query = Query(format: "\(try! T.entityIdProperty()) IN %@", ids as AnyObject)
        return remove(
            query,
            options: options,
            completionHandler: completionHandler
        )
    }
    
    /// Deletes a list of records that matches with the query passed by parameter.
    @discardableResult
    open func remove(
        _ query: Query = Query(),
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)?
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        let writePolicy = options?.writePolicy ?? self.writePolicy
        let operation = RemoveByQueryOperation<T>(
            query: Query(query: query, persistableType: T.self),
            writePolicy: writePolicy,
            sync: sync,
            cache: cache,
            options: options
        )
        let request = operation.execute { result in
            DispatchQueue.main.async {
                completionHandler?(result)
            }
        }
        return request
    }
    
    /// Deletes all the records.
    @discardableResult
    open func removeAll(
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)?
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        return remove(
            options: options,
            completionHandler: completionHandler
        )
    }
    
    /// Sends to the backend all the pending records in the local cache.
    @discardableResult
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use DataStore.push(options:completionHandler:) instead")
    open func push(
        timeout: TimeInterval? = nil,
        completionHandler: UIntErrorTypeArrayCompletionHandler? = nil
    ) -> AnyRequest<Swift.Result<UInt, MultipleErrors>> {
        return push(
            timeout: timeout
        ) { (result: Swift.Result<UInt, MultipleErrors>) in
            switch result {
            case .success(let count):
                completionHandler?(count, nil)
            case .failure(let errors):
                completionHandler?(nil, errors.errors)
            }
        }
    }
    
    /// Sends to the backend all the pending records in the local cache.
    @discardableResult
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use DataStore.push(options:completionHandler:) instead")
    open func push(
        timeout: TimeInterval? = nil,
        completionHandler: ((Swift.Result<UInt, MultipleErrors>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<UInt, MultipleErrors>> {
        return push(
            options: try! Options(timeout: timeout),
            completionHandler: completionHandler
        )
    }
    
    /// Sends to the backend all the pending records in the local cache.
    @discardableResult
    open func push(
        options: Options? = nil,
        completionHandler: ((Swift.Result<UInt, MultipleErrors>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<UInt, MultipleErrors>> {
        let request = MultiRequest<Swift.Result<UInt, MultipleErrors>>()
        Promise<UInt> { resolver in
            if type == .network {
                let error = MultipleErrors(errors: [Error.invalidDataStoreType])
                resolver.reject(error)
            } else {
                let operation = PushOperation<T>(
                    sync: sync,
                    cache: cache,
                    options: options
                )
                request += operation.execute() { result in
                    switch result {
                    case .success(let count):
                        resolver.fulfill(count)
                    case .failure(let errors):
                        resolver.reject(errors)
                    }
                }
            }
        }.done { count in
            request.result = .success(count)
            completionHandler?(request.result!)
        }.catch { error in
            let error = error as! MultipleErrors
            request.result = .failure(error)
            completionHandler?(request.result!)
        }
        return AnyRequest(request)
    }
    
    /// Gets the records from the backend that matches with the query passed by parameter and saves locally in the local cache.
    @discardableResult
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use DataStore.pull(_:deltaSetCompletionHandler:options:completionHandler:) instead")
    open func pull(
        _ query: Query = Query(),
        deltaSetCompletionHandler: ((AnyRandomAccessCollection<T>, AnyRandomAccessCollection<T>) -> Void)? = nil,
        deltaSet: Bool? = nil,
        completionHandler: DataStore<T>.ArrayCompletionHandler? = nil
    ) -> AnyRequest<Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>> {
        return pull(
            query,
            deltaSetCompletionHandler: deltaSetCompletionHandler,
            options: try! Options(deltaSet: deltaSet)
        ) { (result: Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>) in
            switch result {
            case .success(let entities):
                completionHandler?(Array(entities), nil)
            case .failure(let error):
                completionHandler?(nil, error)
            }
        }
    }
    
    /// Gets the records from the backend that matches with the query passed by parameter and saves locally in the local cache.
    @discardableResult
    open func pull(
        _ query: Query = Query(),
        deltaSetCompletionHandler: ((AnyRandomAccessCollection<T>, AnyRandomAccessCollection<T>) -> Void)? = nil,
        options: Options? = nil,
        completionHandler: ((Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>> {
        var request: AnyRequest<Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>>!
        Promise<AnyRandomAccessCollection<T>> { resolver in
            guard type != .network else {
                let error = Error.invalidDataStoreType
                request = AnyRequest(.failure(error))
                resolver.reject(error)
                return
            }
            
            guard self.pendingSyncEntities().count == 0 else {
                let error = Error.invalidOperation(description: "You must push all pending sync items before new data is pulled. Call push() on the data store instance to push pending items, or purge() to remove them.")
                request = AnyRequest(.failure(error))
                resolver.reject(error)
                return
            }
            
            let operation = PullOperation<T>(
                query: Query(query: query, persistableType: T.self),
                deltaSetCompletionHandler: deltaSetCompletionHandler,
                autoPagination: autoPagination,
                readPolicy: .forceNetwork,
                validationStrategy: validationStrategy,
                cache: cache,
                options: try Options(specific: options, general: self.options)
            )
            request = operation.execute(resolver.completionHandler())
        }.done { array in
            completionHandler?(.success(array))
        }.catch { error in
            completionHandler?(.failure(error))
        }
        return request
    }
    
    /// Returns the number of changes not synced yet.
    @available(*, deprecated, message: "Deprecated in version 3.24.0. please use DataStore.pendingSyncCount() instead")
    open func syncCount() -> UInt {
        return UInt(pendingSyncCount())
    }
    
    /// Returns the number of changes not synced yet.
    open func pendingSyncCount() -> Int {
        return sync?.pendingOperationsCount() ?? 0
    }
    
    /// Returns the changes not synced yet.
    open func pendingSyncEntities() -> AnyRandomAccessCollection<PendingOperation> {
        return sync?.pendingOperations(useMultiInsert: false) ?? AnyRandomAccessCollection([])
    }
    
    /// Returns the changes not synced yet, but with the POST requests aggregated to use multi-insert.
    open func pendingSyncOperations() -> AnyRandomAccessCollection<PendingOperation> {
        return sync?.pendingOperations(useMultiInsert: true) ?? AnyRandomAccessCollection([])
    }
    
    open func clearSync() -> Int {
        return sync?.removeAllPendingOperations() ?? 0
    }
    
    /// Calls `push` and then `pull` methods, so it sends all the pending records in the local cache and then gets the records from the backend and saves locally in the local cache.
    @discardableResult
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use DataStore.sync(_:deltaSetCompletionHandler:options:completionHandler:) instead")
    open func sync(
        _ query: Query = Query(),
        deltaSetCompletionHandler: ((AnyRandomAccessCollection<T>, AnyRandomAccessCollection<T>) -> Void)? = nil,
        deltaSet: Bool? = nil,
        completionHandler: UIntArrayCompletionHandler? = nil
    ) -> AnyRequest<Swift.Result<(UInt, [T]), MultipleErrors>> {
        return sync(
            query,
            deltaSetCompletionHandler: deltaSetCompletionHandler,
            deltaSet: deltaSet
        ) { (result: Swift.Result<(UInt, [T]), MultipleErrors>) in
            switch result {
            case .success((let count, let array)):
                completionHandler?(count, array, nil)
            case .failure(let errors):
                completionHandler?(nil, nil, errors.errors)
            }
        }
    }
    
    /// Calls `push` and then `pull` methods, so it sends all the pending records in the local cache and then gets the records from the backend and saves locally in the local cache.
    @discardableResult
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use DataStore.sync(_:deltaSetCompletionHandler:options:completionHandler:) instead")
    open func sync(
        _ query: Query = Query(),
        deltaSetCompletionHandler: ((AnyRandomAccessCollection<T>, AnyRandomAccessCollection<T>) -> Void)? = nil,
        deltaSet: Bool? = nil,
        completionHandler: ((Swift.Result<(UInt, [T]), MultipleErrors>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<(UInt, [T]), MultipleErrors>> {
        let request = sync(
            query,
            deltaSetCompletionHandler: deltaSetCompletionHandler,
            options: try! Options(deltaSet: deltaSet)
        ) { (result: Swift.Result<(UInt, AnyRandomAccessCollection<T>), MultipleErrors>) in
            guard let completionHandler = completionHandler else {
                return
            }
            
            switch result {
            case .success((let count, let results)):
                completionHandler(.success((count, Array(results))))
            case .failure(let error):
                completionHandler(.failure(error))
            }
        }
        return AnyRequest(request) {
            if let result = $0 {
                switch result {
                case .success((let count, let results)):
                    return .success((count, Array(results)))
                case .failure(let error):
                    return .failure(error)
                }
            }
            return nil
        }
    }
    
    /// Calls `push` and then `pull` methods, so it sends all the pending records in the local cache and then gets the records from the backend and saves locally in the local cache.
    @discardableResult
    open func sync(
        _ query: Query = Query(),
        deltaSetCompletionHandler: ((AnyRandomAccessCollection<T>, AnyRandomAccessCollection<T>) -> Void)? = nil,
        options: Options? = nil,
        completionHandler: ((Swift.Result<(UInt, AnyRandomAccessCollection<T>), MultipleErrors>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<(UInt, AnyRandomAccessCollection<T>), MultipleErrors>> {
        let requests = MultiRequest<Swift.Result<(UInt, AnyRandomAccessCollection<T>), MultipleErrors>>()
        Promise<(UInt, AnyRandomAccessCollection<T>)> { resolver in
            guard type != .network else {
                let error = Error.invalidDataStoreType
                requests += LocalRequest(error)
                resolver.reject(error)
                return
            }
            let request = push(
                options: options
            ) { (result: Swift.Result<UInt, MultipleErrors>) in
                switch result {
                case .success(let count):
                    let request = self.pull(
                        query,
                        deltaSetCompletionHandler: deltaSetCompletionHandler,
                        options: options
                    ) { (result: Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>) in
                        switch result {
                        case .success(let array):
                            resolver.fulfill((count, array))
                        case .failure(let error):
                            resolver.reject(error)
                        }
                    }
                    requests.addRequest(request)
                case .failure(let errors):
                    resolver.reject(errors)
                }
            }
            requests += request
        }.done {
            completionHandler?(.success($0))
        }.catch { error in
            if let error = error as? MultipleErrors {
                completionHandler?(.failure(error))
            } else {
                completionHandler?(.failure(MultipleErrors(errors: [error])))
            }
        }
        return AnyRequest(requests)
    }
    
    /// Deletes all the pending changes in the local cache.
    @discardableResult
    @available(*, deprecated, message: "Deprecated in version 3.17.0. Please use DataStore.purge(_:options:completionHandler:) instead")
    open func purge(
        _ query: Query = Query(),
        completionHandler: DataStore<T>.IntCompletionHandler? = nil
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        return purge(
            query
        ) { (result: Swift.Result<Int, Swift.Error>) in
            switch result {
            case .success(let count):
                completionHandler?(count, nil)
            case .failure(let error):
                completionHandler?(nil, error)
            }
        }
    }
    
    /// Deletes all the pending changes in the local cache.
    @discardableResult
    open func purge(
        _ query: Query = Query(),
        options: Options? = nil,
        completionHandler: ((Swift.Result<Int, Swift.Error>) -> Void)? = nil
    ) -> AnyRequest<Swift.Result<Int, Swift.Error>> {
        var request: AnyRequest<Swift.Result<Int, Swift.Error>>!
        Promise<Int> { resolver in
            guard type != .network else {
                let error = Error.invalidDataStoreType
                request = AnyRequest(.failure(error))
                resolver.reject(error)
                return
            }
            let executor = Executor()
            
            let operation = PurgeOperation<T>(
                sync: sync,
                cache: cache,
                options: options
            )
            let requests = MultiRequest<Swift.Result<Int, Swift.Error>>()
            requests += operation.execute { result in
                switch result {
                case .success(let count):
                    executor.execute {
                        requests += self.pull(
                            query,
                            options: options
                        ) { (result: Swift.Result<AnyRandomAccessCollection<T>, Swift.Error>) in
                            switch result {
                            case .success:
                                requests.result = .success(count)
                                resolver.fulfill(count)
                            case .failure(let error):
                                requests.result = .failure(error)
                                resolver.reject(error)
                            }
                        }
                    }
                case .failure(let error):
                    resolver.reject(error)
                }
            }
            request = AnyRequest(requests)
        }.done { count in
            completionHandler?(.success(count))
        }.catch { error in
            completionHandler?(.failure(error))
        }
        return request
    }
    
    /// Clear all data for all collections.
    open class func clearCache(_ tag: String? = nil, client: Client = sharedClient) {
        client.cacheManager.clearAll(tag)
    }

    /// Clear all data for the collection attached to the DataStore.
    open func clearCache(query: Query? = nil, cascadeDelete: Bool = false) {
        cache?.clear(query: query, cascadeDelete: cascadeDelete)
    }
    
    private lazy var channelName: String = {
        return "\(self.client.appKey!).c-\(self.collectionName)"
    }()
    
    private func channelName(forUser user: User) -> String {
        return "\(self.channelName).u-\(user.userId)"
    }
    
    private func realtimeRouter() throws -> RealtimeRouter {
        guard let user = client.activeUser else {
            throw Error.invalidOperation(description: "Active User not found")
        }
        
        guard let realtimeRouter = user.realtimeRouter else {
            throw Error.invalidOperation(description: "Active User not register for realtime")
        }
        
        return realtimeRouter
    }
    
    func execute<Result>(request: HttpRequest<Result>) -> Promise<RealtimeRouter> {
        return Promise<RealtimeRouter> { resolver in
            do {
                let realtimeRouter = try self.realtimeRouter()
                request.execute() { (data, response, error) in
                    if let response = response, response.isOK {
                        resolver.fulfill(realtimeRouter)
                    } else {
                        resolver.reject(buildError(data, response, error, self.client))
                    }
                }
            } catch {
                resolver.reject(error)
            }
        }
    }
    
    /**
     Subscribe and start listening to changes in the collection
     */
    @discardableResult
    open func subscribe(
        options: Options? = nil,
        subscription: @escaping () -> Void,
        onNext: @escaping (T) -> Void,
        onStatus: @escaping (RealtimeStatus) -> Void,
        onError: @escaping (Swift.Error) -> Void
    ) -> AnyRequest<Swift.Result<Void, Swift.Error>> {
        let client = options?.client ?? self.client
        let request = client.networkRequestFactory.stream.buildAppDataSubscribe(
            collectionName: collectionName,
            deviceId: deviceId,
            options: options,
            resultType: Swift.Result<Void, Swift.Error>.self
        )
        execute(
            request: request
        ).done { (realtimeRouter) -> Void in
            let onNext: (Any?) -> Void = {
                if let dict = $0 as? [String : Any],
                    let obj = try? client.jsonParser.parseObject(T.self, from: dict)
                {
                    self.cache?.save(entity: obj)
                    onNext(obj)
                }
            }
            realtimeRouter.subscribe(
                channel: self.channelName,
                context: self,
                onNext: onNext,
                onStatus: onStatus,
                onError: onError
            )
            let client = options?.client ?? self.client
            if let activeUser = client.activeUser {
                realtimeRouter.subscribe(
                    channel: self.channelName(forUser: activeUser),
                    context: self,
                    onNext: onNext,
                    onStatus: onStatus,
                    onError: onError
                )
            }
        }.done {
            subscription()
        }.catch { error in
            onError(error)
        }
        return AnyRequest(request)
    }
    
    /**
     Unsubscribe and stop listening changes in the collection
     */
    @discardableResult
    open func unsubscribe(
        options: Options? = nil,
        completionHandler: @escaping (Swift.Result<Void, Swift.Error>) -> Void
    ) -> AnyRequest<Swift.Result<Void, Swift.Error>> {
        let request = client.networkRequestFactory.stream.buildAppDataUnSubscribe(
            collectionName: collectionName,
            deviceId: deviceId,
            options: options,
            resultType: Swift.Result<Void, Swift.Error>.self
        )
        execute(
            request: request
        ).done { (realtimeRouter) -> Void in
            realtimeRouter.unsubscribe(channel: self.channelName, context: self)
            let client = options?.client ?? self.client
            if let activeUser = client.activeUser {
                realtimeRouter.unsubscribe(channel: self.channelName(forUser: activeUser), context: self)
            }
        }.done {
            completionHandler(.success($0))
        }.catch { error in
            completionHandler(.failure(error))
        }
        return AnyRequest(request)
    }
    
    public func observe(_ query: Query? = nil, completionHandler: @escaping (CollectionChange<AnyRandomAccessCollection<T>>) -> Void) -> AnyNotificationToken? {
        return cache?.observe(query, completionHandler: completionHandler)
    }

}

extension DataStore: Hashable {
    
    public func hash(into hasher: inout Hasher) {
        hasher.combine(uuid)
    }
    
    public static func ==(lhs: DataStore<T>, rhs: DataStore<T>) -> Bool {
        return lhs.uuid == rhs.uuid
    }
    
}