Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MLIBZ-3022: Multi-Record Insert #359

Merged
merged 4 commits into from
May 28, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Kinvey/Kinvey.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
5781D1311CE3ADBC00369F40 /* ErrorTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5781D1301CE3ADBC00369F40 /* ErrorTestCase.swift */; };
5781D1341CE3B0FB00369F40 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5781D12E1CE3ACA000369F40 /* Localizable.strings */; };
5781D1361CE3D0BA00369F40 /* FileTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5781D1351CE3D0BA00369F40 /* FileTestCase.swift */; };
57833531228A220800949231 /* SaveMultiOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57833530228A220700949231 /* SaveMultiOperation.swift */; };
5783B5071C1910B00077F8A6 /* JsonObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5783B5061C1910B00077F8A6 /* JsonObject.swift */; };
5783BA3520F722DC0090D0BD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57BEAE2E1C98805E00479206 /* QuartzCore.framework */; };
5783BA3620F722DC0090D0BD /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57BEAE2C1C98805600479206 /* CoreGraphics.framework */; };
Expand Down Expand Up @@ -365,6 +366,7 @@
57C6F70C20E30ADF0090935C /* ObjectMapperSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C6F70B20E30ADF0090935C /* ObjectMapperSupport.swift */; };
57C71DC81C3EFBF900B1BEF2 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C71DC71C3EFBF900B1BEF2 /* Request.swift */; };
57C731FA1F57721700B67C13 /* MacOSOnlyTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C731F91F57721700B67C13 /* MacOSOnlyTestCase.swift */; };
57CA7A64228CE24000479E49 /* MultipleRandomAccessCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CA7A63228CE24000479E49 /* MultipleRandomAccessCollection.swift */; };
57CCBCBE20D2C6B300197CC2 /* CacheMigrationTestCaseStep2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D6433D1CA326DE00F6D16E /* CacheMigrationTestCaseStep2.swift */; };
57CCBCBF20D2C6C400197CC2 /* CacheMigrationTestCaseData.zip in Resources */ = {isa = PBXBuildFile; fileRef = 573CC99C20D19AB900BDF726 /* CacheMigrationTestCaseData.zip */; };
57CCBCC020D2CA0B00197CC2 /* KinveyTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57A27C901C178F18000DF951 /* KinveyTestCase.swift */; };
Expand Down Expand Up @@ -1000,6 +1002,7 @@
5781D12E1CE3ACA000369F40 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
5781D1301CE3ADBC00369F40 /* ErrorTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTestCase.swift; sourceTree = "<group>"; };
5781D1351CE3D0BA00369F40 /* FileTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileTestCase.swift; sourceTree = "<group>"; };
57833530228A220700949231 /* SaveMultiOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveMultiOperation.swift; sourceTree = "<group>"; };
5783B5061C1910B00077F8A6 /* JsonObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonObject.swift; sourceTree = "<group>"; };
5783BA4120F722DC0090D0BD /* KinveyTests RemoveAll Memory Leak.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "KinveyTests RemoveAll Memory Leak.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
5783BA4320F723270090D0BD /* RemoveAllMemoryLeakTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAllMemoryLeakTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1105,6 +1108,7 @@
57C6F70B20E30ADF0090935C /* ObjectMapperSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectMapperSupport.swift; sourceTree = "<group>"; };
57C71DC71C3EFBF900B1BEF2 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
57C731F91F57721700B67C13 /* MacOSOnlyTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacOSOnlyTestCase.swift; sourceTree = "<group>"; };
57CA7A63228CE24000479E49 /* MultipleRandomAccessCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleRandomAccessCollection.swift; sourceTree = "<group>"; };
57CCBCD620D2CD3F00197CC2 /* KinveyTests Migration Database macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "KinveyTests Migration Database macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
57CDF45D22653DBC006596A7 /* AutoDataStoreSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDataStoreSpec.swift; sourceTree = "<group>"; };
57CDF46022654226006596A7 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Quick.framework; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1528,6 +1532,7 @@
57F91C281D6E2C020012850A /* CountOperation.swift */,
570BD2FC1E845E7A000341C9 /* AggregateOperation.swift */,
574B0FAA1C729EC900CDC48F /* SaveOperation.swift */,
57833530228A220700949231 /* SaveMultiOperation.swift */,
57FF4F6C1CCFE71B002947FF /* RemoveOperation.swift */,
574B0FAC1C729F3300CDC48F /* RemoveByQueryOperation.swift */,
5714EBB11CCEEAF9001E3ECF /* RemoveByIdOperation.swift */,
Expand Down Expand Up @@ -1745,6 +1750,7 @@
57B257B820929621005B329C /* DeviceInfo.swift */,
57C6F70B20E30ADF0090935C /* ObjectMapperSupport.swift */,
57E853A320FECE6500042C14 /* RealmSupport.swift */,
57CA7A63228CE24000479E49 /* MultipleRandomAccessCollection.swift */,
);
path = Kinvey;
sourceTree = "<group>";
Expand Down Expand Up @@ -3466,6 +3472,7 @@
57AC52881D395F7D000887D3 /* AuthSource.swift in Sources */,
57E1C3A31C17B3FF00578974 /* Query.swift in Sources */,
570BD2FD1E845E7A000341C9 /* AggregateOperation.swift in Sources */,
57CA7A64228CE24000479E49 /* MultipleRandomAccessCollection.swift in Sources */,
57A27C9E1C178FB5000DF951 /* Client.swift in Sources */,
57E7C7A51C504AC500848748 /* Cache.swift in Sources */,
573851AC1D47C7EB00E4712A /* FileCache.swift in Sources */,
Expand Down Expand Up @@ -3505,6 +3512,7 @@
577E6FA81D18E45F00B5DA36 /* Executor.swift in Sources */,
57B768811D10C0C70086AA38 /* Entity.swift in Sources */,
57A27C9C1C178FAC000DF951 /* Kinvey.swift in Sources */,
57833531228A220800949231 /* SaveMultiOperation.swift in Sources */,
57DB87E81C62B0F6002BA684 /* Data.swift in Sources */,
57E1C3B11C18156700578974 /* HttpRequestFactory.swift in Sources */,
573851AE1D47C7F800E4712A /* RealmFileCache.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Kinvey/Kinvey/Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal protocol CacheType: class {

func save(entity: Type)

func save(entities: AnyRandomAccessCollection<Type>, syncQuery: SyncQuery?)
func save<C>(entities: C, syncQuery: SyncQuery?) where C : Collection, C.Element == Type

func save(syncQuery: SyncQuery)

Expand Down Expand Up @@ -149,7 +149,7 @@ class AnyCache<T: Persistable>: CacheType {
private let _getTTL: () -> TimeInterval?
private let _setTTL: (TimeInterval?) -> Void
private let _saveEntity: (T) -> Void
private let _saveEntities: (AnyRandomAccessCollection<Type>, SyncQuery?) -> Void
private let _saveEntities: (AnyCollection<Type>, SyncQuery?) -> Void
private let _saveSyncQuery: (SyncQuery) -> Void
private let _findById: (String) -> T?
private let _findByQuery: (Query) -> AnyRandomAccessCollection<Type>
Expand Down Expand Up @@ -206,8 +206,8 @@ class AnyCache<T: Persistable>: CacheType {
_saveEntity(entity)
}

func save(entities: AnyRandomAccessCollection<Type>, syncQuery: SyncQuery?) {
_saveEntities(entities, syncQuery)
func save<C>(entities: C, syncQuery: SyncQuery?) where C : Collection, C.Element == Type {
_saveEntities(AnyCollection(entities), syncQuery)
}

func save(syncQuery: SyncQuery) {
Expand Down
23 changes: 23 additions & 0 deletions Kinvey/Kinvey/DataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,29 @@ open class DataStore<T: Persistable> where T: NSObject {
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(
Expand Down
33 changes: 31 additions & 2 deletions Kinvey/Kinvey/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD
/// Constant for 400 response where the parameter value is a BL runtime error
case blRuntimeError = "BLRuntimeError"

/// Constant for 400 response where the feature is not available
case featureUnavailable = "FeatureUnavailable"

/// Constant for 500 response where an internal error happened and another request should be made to retry
case kinveyInternalErrorRetry = "KinveyInternalErrorRetry"

}

/// Error where Object ID is required.
Expand Down Expand Up @@ -104,6 +110,11 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD
/// Error when a BL (Business Logic) Runtime Error occurs
case blRuntime(debug: String, description: String, stack: String)

/// Error when a feature is not available
case featureUnavailable(debug: String, description: String)

case kinveyInternalErrorRetry(debug: String, description: String)

/// Error localized description.
public var description: String {
let bundle = Bundle(for: Client.self)
Expand All @@ -122,7 +133,9 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD
.parameterValueOutOfRange(_, let description),
.invalidCredentials(_, _, _, let description),
.micAuth(_, let description),
.blRuntime(_, let description, _):
.blRuntime(_, let description, _),
.featureUnavailable(_, let description),
.kinveyInternalErrorRetry(_, let description):
return description
case .objectIdMissing:
return NSLocalizedString("Error.objectIdMissing", bundle: bundle, comment: "")
Expand Down Expand Up @@ -164,7 +177,9 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD
.entityNotFound(let debug, _),
.parameterValueOutOfRange(let debug, _),
.invalidCredentials(_, _, let debug, _),
.blRuntime(let debug, _, _):
.blRuntime(let debug, _, _),
.featureUnavailable(let debug, _),
.kinveyInternalErrorRetry(let debug, _):
return debug
default:
return description
Expand Down Expand Up @@ -312,3 +327,17 @@ extension NSException {
}

}

public struct MultiSaveError: Swift.Error, Codable {

let index: Int
let code: Int
let message: String

enum CodingKeys: String, CodingKey {
case index
case code
case message = "errmsg"
}

}
7 changes: 6 additions & 1 deletion Kinvey/Kinvey/HttpRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ extension String {
}

/// REST API Version used in the REST calls.
public let restApiVersion = 4
public var restApiVersion = 4
heyzooi marked this conversation as resolved.
Show resolved Hide resolved

enum Body {

Expand Down Expand Up @@ -532,5 +532,10 @@ internal class HttpRequest<Result>: TaskProgressRequest, Request {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try! JSONSerialization.data(withJSONObject: json)
}

func setBody(json: [[String : Any]]) {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try! JSONSerialization.data(withJSONObject: json)
}

}
19 changes: 19 additions & 0 deletions Kinvey/Kinvey/HttpRequestFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,25 @@ class HttpRequestFactory: RequestFactory {
return request
}

func buildAppDataSave<S: Sequence, T: Persistable, Result>(
_ persistable: S,
options: Options?,
resultType: Result.Type
) -> HttpRequest<Result> where S.Element == T {
let collectionName = try! T.collectionName()
let client = options?.client ?? self.client
let bodyObject = try! client.jsonParser.toJSON(persistable)
let request = HttpRequest<Result>(
httpMethod: .post,
endpoint: Endpoint.appData(client: client, collectionName: collectionName),
credential: client.activeUser,
options: options
)

request.setBody(json: bodyObject)
return request
}

func buildAppDataRemoveByQuery<Result>(
collectionName: String,
query: Query,
Expand Down
4 changes: 4 additions & 0 deletions Kinvey/Kinvey/HttpResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ struct HttpResponse: Response {
return response.statusCode == 405
}

var isInternalServerError: Bool {
return response.statusCode == 500
}

var etag: String? {
return allHeaderFields?["etag"] as? String
}
Expand Down
5 changes: 5 additions & 0 deletions Kinvey/Kinvey/JSONParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public protocol JSONParser {

func toJSON<UserType: User>(_ user: UserType) throws -> [String : Any]
func toJSON<T>(_ object: T) throws -> [String : Any] where T: JSONEncodable
func toJSON<S: Sequence, T>(_ sequence: S) throws -> [[String : Any]] where T: JSONEncodable, S.Element == T

}

Expand Down Expand Up @@ -195,5 +196,9 @@ class DefaultJSONParser: JSONParser {
func toJSON<T>(_ object: T) throws -> [String : Any] where T: JSONEncodable {
return try object.encode()
}

func toJSON<S, T>(_ sequence: S) throws -> [[String : Any]] where S : Sequence, T : JSONEncodable, T == S.Element {
return try sequence.map { try $0.encode() }
}

}
19 changes: 19 additions & 0 deletions Kinvey/Kinvey/Kinvey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,17 @@ func buildError(
description: description,
stack: stack.replacingOccurrences(of: "\\n", with: "\n")
)
} else if let response = response,
response.isBadRequest,
let json = json,
json["error"] == Error.Keys.featureUnavailable.rawValue,
let debug = json["debug"],
let description = json["description"]
{
return Error.featureUnavailable(
debug: debug,
description: description
)
} else if let response = response,
response.isNotFound,
let json = json,
Expand All @@ -442,6 +453,14 @@ func buildError(
let description = json["description"]
{
return Error.entityNotFound(debug: debug, description: description)
} else if let response = response,
response.isInternalServerError,
let json = json,
json["error"] == Error.Keys.kinveyInternalErrorRetry.rawValue,
let debug = json["debug"],
let description = json["description"]
{
return Error.kinveyInternalErrorRetry(debug: debug, description: description)
} else if let response = response,
let data = data,
let json = try? client.jsonParser.parseDictionary(from: data)
Expand Down
1 change: 1 addition & 0 deletions Kinvey/Kinvey/LocalResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class LocalResponse: Response {
var isForbidden = false
var isNotFound = false
var isMethodNotAllowed = false
var isInternalServerError = false

var etag: String? = nil
var contentTypeIsJson: Bool = false
Expand Down
42 changes: 42 additions & 0 deletions Kinvey/Kinvey/MultipleRandomAccessCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// MultipleRandomAccessCollection.swift
// Kinvey
//
// Created by Victor Hugo Carvalho Barros on 2019-05-15.
// Copyright © 2019 Kinvey. All rights reserved.
//

import Foundation

struct MultipleRandomAccessCollection<T>: RandomAccessCollection {

typealias Element = T
typealias Index = Array<T>.Index
typealias SubSequence = Array<T>.SubSequence
typealias Indices = Array<T>.Indices

let begin: AnyRandomAccessCollection<T>
let end: AnyRandomAccessCollection<T>

init<C1, C2>(_ begin: C1, _ end: C2) where C1: RandomAccessCollection, C2: RandomAccessCollection, C1.Element == T, C2.Element == T {
self.begin = AnyRandomAccessCollection(begin)
self.end = AnyRandomAccessCollection(end)
}

var startIndex: Index {
return 0
}

var endIndex: Index {
return begin.count + end.count
}

subscript(position: Index) -> T {
if position < begin.count {
return begin[AnyIndex(position)]
} else {
return end[AnyIndex(position - begin.count)]
}
}

}
9 changes: 9 additions & 0 deletions Kinvey/Kinvey/ObjectMapperJSONParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,14 @@ class ObjectMapperJSONParser: JSONParser {
}
return object.toJSON()
}

func toJSON<S, T>(_ sequence: S) throws -> [[String : Any]] where S : Sequence, T : JSONEncodable, T == S.Element {
return sequence.compactMap {
guard let object = $0 as? BaseMappable else {
return nil
}
return object.toJSON()
}
}

}
2 changes: 1 addition & 1 deletion Kinvey/Kinvey/RealmCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ internal class RealmCache<T: Persistable>: Cache<T>, CacheType where T: NSObject
}
}

func save(entities: AnyRandomAccessCollection<T>, syncQuery: SyncQuery?) {
func save<C>(entities: C, syncQuery: SyncQuery?) where C : Collection, C.Element == T {
signpost(.begin, log: osLog, name: "Save Entities (Generics)")
defer {
signpost(.end, log: osLog, name: "Save Entities (Generics)")
Expand Down
6 changes: 6 additions & 0 deletions Kinvey/Kinvey/RequestFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ protocol RequestFactory {
resultType: Result.Type
) -> HttpRequest<Result>

func buildAppDataSave<S: Sequence, T: Persistable, Result>(
_ persistable: S,
options: Options?,
resultType: Result.Type
) -> HttpRequest<Result> where S.Element == T

func buildAppDataRemoveByQuery<Result>(
collectionName: String,
query: Query,
Expand Down
1 change: 1 addition & 0 deletions Kinvey/Kinvey/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal protocol Response {
var isForbidden: Bool { get }
var isNotFound: Bool { get }
var isMethodNotAllowed: Bool { get }
var isInternalServerError: Bool { get }

var etag: String? { get }
var contentTypeIsJson: Bool { get }
Expand Down
Loading