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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Amplify.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
212CE70F23E9E991007D8E71 /* ModelDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212CE70A23E9E991007D8E71 /* ModelDecorator.swift */; };
212CE71123E9EA6A007D8E71 /* ModelField+GraphQL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212CE71023E9EA6A007D8E71 /* ModelField+GraphQL.swift */; };
212CE71323E9F2ED007D8E71 /* DirectiveNameDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212CE71223E9F2ED007D8E71 /* DirectiveNameDecorator.swift */; };
213481D0242A6040001966DE /* AnyQueryPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213481CF242A603F001966DE /* AnyQueryPredicate.swift */; };
213481D2242A63AA001966DE /* QueryOperator+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 213481D1242A63AA001966DE /* QueryOperator+Codable.swift */; };
21409C552384C55D000A53C9 /* LabelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C542384C55D000A53C9 /* LabelType.swift */; };
21409C5A2384C57D000A53C9 /* GraphQLMutationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C572384C57D000A53C9 /* GraphQLMutationType.swift */; };
21409C5B2384C57D000A53C9 /* GraphQLQueryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C582384C57D000A53C9 /* GraphQLQueryType.swift */; };
Expand Down Expand Up @@ -579,6 +581,8 @@
212CE71B23EA1847007D8E71 /* GraphQLListQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLListQueryTests.swift; sourceTree = "<group>"; };
212CE71C23EA1847007D8E71 /* GraphQLSyncQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLSyncQueryTests.swift; sourceTree = "<group>"; };
212CE72023EA184F007D8E71 /* GraphQLSubscriptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLSubscriptionTests.swift; sourceTree = "<group>"; };
213481CF242A603F001966DE /* AnyQueryPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyQueryPredicate.swift; sourceTree = "<group>"; };
213481D1242A63AA001966DE /* QueryOperator+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryOperator+Codable.swift"; sourceTree = "<group>"; };
21409C4C23847E41000A53C9 /* LabelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelType.swift; sourceTree = "<group>"; };
21409C542384C55D000A53C9 /* LabelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelType.swift; sourceTree = "<group>"; };
21409C572384C57D000A53C9 /* GraphQLMutationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLMutationType.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1792,9 +1796,11 @@
B98E9D042372236200934B51 /* Query */ = {
isa = PBXGroup;
children = (
213481CF242A603F001966DE /* AnyQueryPredicate.swift */,
B98E9D062372236200934B51 /* ModelKey.swift */,
B98E9D0A2372236200934B51 /* QueryField.swift */,
B98E9D072372236200934B51 /* QueryOperator.swift */,
213481D1242A63AA001966DE /* QueryOperator+Codable.swift */,
B98E9D0B2372236200934B51 /* QueryOperator+Equatable.swift */,
B98E9D082372236200934B51 /* QueryPredicate.swift */,
B98E9D0C2372236200934B51 /* QueryPredicate+Equatable.swift */,
Expand Down Expand Up @@ -3703,6 +3709,7 @@
FAA2E8BE23A00BD600E420EA /* AmplifyAPICategory.swift in Sources */,
210DBC142332B3C6009B9E51 /* StorageGetURLOperation.swift in Sources */,
95DAAB2A237E63370028544F /* Pose.swift in Sources */,
213481D0242A6040001966DE /* AnyQueryPredicate.swift in Sources */,
FA09B94D2322CC05000E064D /* StorageCategoryConfiguration.swift in Sources */,
950A26DF23D15D9800D92B19 /* SpeechToTextResult.swift in Sources */,
B92E03AF2367CE7A006CEB8D /* DataStoreCategoryBehavior.swift in Sources */,
Expand Down Expand Up @@ -3744,6 +3751,7 @@
95DAAB31237E63370028544F /* PartOfSpeech.swift in Sources */,
B9FAA13A238BBADE009414B4 /* List+Combine.swift in Sources */,
FA8EE77B2386271A0097E4F1 /* Model+AnyModel.swift in Sources */,
213481D2242A63AA001966DE /* QueryOperator+Codable.swift in Sources */,
FA56F72522B14B6A0039754A /* Resumable.swift in Sources */,
FAA2E8C023A00C6500E420EA /* AmplifyAPICategory+APICategory.swift in Sources */,
21FFF997230C96CB005878EA /* StorageRemoveOperation.swift in Sources */,
Expand Down
89 changes: 89 additions & 0 deletions Amplify/Categories/DataStore/Query/AnyQueryPredicate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Type-erased wrapper of for encoding and decoding `QueryPredicate`
struct AnyQueryPredicate: Codable {
var base: QueryPredicate
init(_ base: QueryPredicate) {
self.base = base
}

private enum CodingKeys: CodingKey {
case type, base
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(QueryPredicateType.self, forKey: .type)
self.base = try type.metatype.init(from: decoder)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type(of: base).type, forKey: .type)
try base.encode(to: encoder)
}
}

/// Adds JSON serialization behavior for `AnyQueryPredicate`
extension AnyQueryPredicate {
/// Converts the `AnyQueryPredicate` instance to a JSON object as `String`.
/// - Parameters:
/// - encoder: an optional JSONEncoder to use to encode the model. Defaults to `JSONEncoder()`, using a
/// custom date formatter that encodes ISO8601 dates with fractional seconds
/// - Returns: the JSON representation of the `Model`
/// - seealso: https://developer.apple.com/documentation/foundation/jsonencoder/2895034-encode
public func toJSON(encoder: JSONEncoder? = nil) throws -> String {
let resolvedEncoder: JSONEncoder
if let encoder = encoder {
resolvedEncoder = encoder
} else {
resolvedEncoder = JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy)
}

let data = try resolvedEncoder.encode(self)
guard let json = String(data: data, encoding: .utf8) else {
throw DataStoreError.decodingError(
"Invalid UTF-8 Data object. Could not convert the encoded Model into a valid UTF-8 JSON string",
"Check if your QueryPredicate doesn't contain any value with invalid UTF-8 characters."
)
}

return json
}

/// De-serialize a JSON string into an instance of the concrete type that conforms
/// to the `AnyQueryPredicate` struct.
///
/// - Parameters:
/// - json: a valid JSON object as `String`
/// - decoder: an optional JSONDecoder to use to decode the model. Defaults to `JSONDecoder()`, using a
/// custom date formatter that decodes ISO8601 dates both with and without fractional seconds
/// - Returns: an instance of the concrete type conforming to `Model`
/// - Throws: `DecodingError.dataCorrupted` in case data is not a valid JSON or any
/// other decoding specific error that `JSONDecoder.decode()` might throw.
public static func from(json: String,
decoder: JSONDecoder? = nil) throws -> Self {
let resolvedDecoder: JSONDecoder
if let decoder = decoder {
resolvedDecoder = decoder
} else {
resolvedDecoder = JSONDecoder(dateDecodingStrategy: ModelDateFormatting.decodingStrategy)
}

guard let data = json.data(using: .utf8) else {
throw DataStoreError.decodingError(
"Invalid JSON string. Could not convert the passed JSON string into a UTF-8 Data object",
"Ensure the JSON doesn't contain any invalid UTF-8 data:\n\n\(json)"
)
}

return try resolvedDecoder.decode(Self.self, from: data)
}
}
176 changes: 176 additions & 0 deletions Amplify/Categories/DataStore/Query/QueryOperator+Codable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Custom decoder/encoder to store and retrieve a `QueryOperator`
extension QueryOperator: Codable {

private enum CodingKeys: String, CodingKey {
/// QueryOperator type
case base

/// Core types that conform to the `Persistable` protocol, including nil case
case persistableType

/// Value with type specified by `.persistableType`
case firstValue

/// Additional value with type specified by `.persistableType`
case secondValue
}
private enum Base: String, Codable {
case notEqual
case equals
case lessOrEqual
case lessThan
case greaterOrEqual
case greaterThan
case contains
case between
case beginsWith
}
private enum PersistableType: String, Codable {
case bool
case date
case double
case int
case string
case null
}

// swiftlint:disable cyclomatic_complexity
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let queryOperator = try container.decode(Base.self, forKey: .base)
let persistableType = try container.decode(PersistableType.self, forKey: .persistableType)
let value: Persistable?
switch persistableType {
case .bool:
value = try container.decode(Bool.self, forKey: .firstValue)
case .date:
value = try container.decode(Date.self, forKey: .firstValue)
case .double:
value = try container.decode(Double.self, forKey: .firstValue)
case .int:
value = try container.decode(Int.self, forKey: .firstValue)
case .string:
value = try container.decode(String.self, forKey: .firstValue)
case .null:
value = nil
}
switch queryOperator {
case .notEqual:
self = .notEqual(value)
return
case .equals:
self = .equals(value)
return
case .lessOrEqual:
if let value = value {
self = .lessOrEqual(value)
return
}
case .lessThan:
if let value = value {
self = .lessThan(value)
return
}
case .greaterOrEqual:
if let value = value {
self = .greaterOrEqual(value)
return
}
case .greaterThan:
if let value = value {
self = .greaterThan(value)
return
}
case .contains:
if let value = value as? String {
self = .contains(value)
return
}
case .between:
if let value = value {
let secondValue = try container.decode(String.self, forKey: .secondValue)
self = .between(start: value, end: secondValue)
return
}
case .beginsWith:
if let value = value as? String {
self = .beginsWith(value)
return
}
}

throw DataStoreError.decodingError("Error decoding QueryOperator",
"Make sure the conforming types are correct for the QueryOperator used.")
}

public func encode(to encoder: Encoder) throws {
switch self {
case .notEqual(let value):
try encodePersistable(encoder, queryOperator: Base.notEqual, firstValue: value)
case .equals(let value):
try encodePersistable(encoder, queryOperator: Base.equals, firstValue: value)
case .lessOrEqual(let value):
try encodePersistable(encoder, queryOperator: Base.lessOrEqual, firstValue: value)
case .lessThan(let value):
try encodePersistable(encoder, queryOperator: Base.lessOrEqual, firstValue: value)
case .greaterOrEqual(let value):
try encodePersistable(encoder, queryOperator: Base.greaterOrEqual, firstValue: value)
case .greaterThan(let value):
try encodePersistable(encoder, queryOperator: Base.greaterThan, firstValue: value)
case .contains(let value):
try encodePersistable(encoder, queryOperator: Base.contains, firstValue: value)
case .between(let start, let end):
try encodePersistable(encoder, queryOperator: Base.between, firstValue: start, secondValue: end)
case .beginsWith(let value):
try encodePersistable(encoder, queryOperator: Base.beginsWith, firstValue: value)
}
}

private func encodePersistable(_ encoder: Encoder,
queryOperator: Base,
firstValue: Persistable?,
secondValue: Persistable? = nil) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(queryOperator, forKey: .base)

if let value = firstValue as? Bool {
try container.encode(PersistableType.bool, forKey: .persistableType)
try container.encode(value, forKey: .firstValue)
} else if let value = firstValue as? Date {
try container.encode(PersistableType.date, forKey: .persistableType)
try container.encode(value, forKey: .firstValue)
} else if let value = firstValue as? Double {
try container.encode(PersistableType.double, forKey: .persistableType)
try container.encode(value, forKey: .firstValue)
} else if let value = firstValue as? Int {
try container.encode(PersistableType.int, forKey: .persistableType)
try container.encode(value, forKey: .firstValue)
} else if let value = firstValue as? String {
try container.encode(PersistableType.string, forKey: .persistableType)
try container.encode(value, forKey: .firstValue)
} else {
try container.encode(PersistableType.null, forKey: .persistableType)
}

if let secondValue = secondValue as? Bool {
try container.encode(secondValue, forKey: .secondValue)
} else if let secondValue = secondValue as? Date {
try container.encode(secondValue, forKey: .secondValue)
} else if let secondValue = secondValue as? Double {
try container.encode(secondValue, forKey: .secondValue)
} else if let secondValue = secondValue as? Int {
try container.encode(secondValue, forKey: .secondValue)
} else if let secondValue = secondValue as? String {
try container.encode(secondValue, forKey: .secondValue)
}
}
}
52 changes: 49 additions & 3 deletions Amplify/Categories/DataStore/Query/QueryPredicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,29 @@
import Foundation

/// Protocol that indicates concrete types conforming to it can be used a predicate member.
public protocol QueryPredicate {}
public protocol QueryPredicate: Codable {
static var type: QueryPredicateType { get }
}

public enum QueryPredicateGroupType: String {
/// List of possible `QueryPredicate` types
public enum QueryPredicateType: String, Codable {
case group
case constant
case operation

var metatype: QueryPredicate.Type {
switch self {
case .group:
return QueryPredicateGroup.self
case .constant:
return QueryPredicateConstant.self
case .operation:
return QueryPredicateOperation.self
}
}
}

public enum QueryPredicateGroupType: String, Codable {
case and
case or
case not
Expand All @@ -26,11 +46,14 @@ public func not<Predicate: QueryPredicate>(_ predicate: Predicate) -> QueryPredi
/// The case `.all` is a predicate used as an argument to select all of a single modeltype. We
/// chose `.all` instead of `nil` because we didn't want to use the implicit nature of `nil` to
/// specify an action applies to an entire data set.
public enum QueryPredicateConstant: QueryPredicate {
public enum QueryPredicateConstant: String, QueryPredicate {
public static var type: QueryPredicateType = .constant

case all
}

public class QueryPredicateGroup: QueryPredicate {
public static var type: QueryPredicateType = .group

public internal(set) var type: QueryPredicateGroupType
public internal(set) var predicates: [QueryPredicate]
Expand Down Expand Up @@ -72,9 +95,32 @@ public class QueryPredicateGroup: QueryPredicate {
public static prefix func ! (rhs: QueryPredicateGroup) -> QueryPredicateGroup {
return not(rhs)
}

/// Provide conformance to `Codable`

enum CodingKeys: String, CodingKey {
case type
case predicates
}

/// Decode `type` and `predicates`. Array of predicates are first decoded using `AnyQueryPredicate` wrapper, and
/// then the inner `QueryPredicate` is retrieved.
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(QueryPredicateGroupType.self, forKey: .type)
self.predicates = try container.decode([AnyQueryPredicate].self, forKey: .predicates).map { $0.base }
}

/// Encode `type` and `predicates`. Array of predicates are encoded as an array of `AnyQueryPredicate`'s.
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(predicates.map(AnyQueryPredicate.init), forKey: .predicates)
}
}

public class QueryPredicateOperation: QueryPredicate {
public static var type: QueryPredicateType = .operation

public let field: String
public let `operator`: QueryOperator
Expand Down
Loading