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

GraphQLDocument Builder #309

Merged
merged 4 commits into from
Feb 12, 2020
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
198 changes: 123 additions & 75 deletions Amplify.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,4 @@ public protocol APICategoryGraphQLBehavior: class {
func subscribe<R: Decodable>(request: GraphQLRequest<R>,
listener: GraphQLSubscriptionOperation<R>.EventListener?)
-> GraphQLSubscriptionOperation<R>

// MARK: - GraphQL operations without a specified type

/// Performs a GraphQL mutate for the `AnyModel` item. This operation will be asynchronous, with the callback
/// accessible both locally and via the Hub.
///
/// - Parameters:
/// - model: The instance of the `AnyModel`.
/// - type: The type of mutation to apply on the instance of `AnyModel`.
/// - listener: The event listener for the operation
/// - Returns: The AmplifyOperation being enqueued.
func mutate(ofAnyModel anyModel: AnyModel,
type: GraphQLMutationType,
listener: GraphQLOperation<AnyModel>.EventListener?) -> GraphQLOperation<AnyModel>

/// An internal method used by Plugins to perform initial subscriptions on registered model types to keep them in
/// sync with DataStore.
///
/// - Parameters:
/// - modelType: The type of the model to subscribe to, as the `Model` protocol rather than the concrete type
/// - subscriptionType: The type of subscription (onCreate, onUpdate, onDelete) to subscribe to
/// - Returns: The AmplifyOperation being enqueued
func subscribe(toAnyModelType modelType: Model.Type,
subscriptionType: GraphQLSubscriptionType,
listener: GraphQLSubscriptionOperation<AnyModel>.EventListener?)
-> GraphQLSubscriptionOperation<AnyModel>

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,4 @@ extension AmplifyAPICategory: APICategoryGraphQLBehavior {
-> GraphQLSubscriptionOperation<R> {
plugin.subscribe(request: request, listener: listener)
}

// MARK: - GraphQL operations without a specified type

public func mutate(ofAnyModel anyModel: AnyModel,
type: GraphQLMutationType,
listener: GraphQLOperation<AnyModel>.EventListener?) -> GraphQLOperation<AnyModel> {
plugin.mutate(ofAnyModel: anyModel, type: type, listener: listener)
}

public func subscribe(toAnyModelType modelType: Model.Type,
subscriptionType: GraphQLSubscriptionType,
listener: GraphQLSubscriptionOperation<AnyModel>.EventListener?)
-> GraphQLSubscriptionOperation<AnyModel> {
plugin.subscribe(toAnyModelType: modelType, subscriptionType: subscriptionType, listener: listener)
}

}
8 changes: 5 additions & 3 deletions Amplify/Categories/DataStore/GraphQL/GraphQLQueryType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
// SPDX-License-Identifier: Apache-2.0
//

/// Defines the type of query, either a `list` which returns multiple results
/// and can optionally use filters or a `get`, which aims to fetch one result
/// identified by its `id`.
/// Defines the type of query,
/// `list` which returns multiple results and can optionally use filters
/// `get`, which aims to fetch one result identified by its `id`.
/// `sync`, similar to `list` and returns results with optionally specifically a point in time
public enum GraphQLQueryType: String {
case get
case list
case sync
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ extension String {
public func camelCased() -> String {
return prefix(1).lowercased() + dropFirst()
}

/// Appends "s" to the end of the string to represent the pluralized form.
public func pluralize() -> String {
return self + "s"
}
}
25 changes: 25 additions & 0 deletions Amplify/Core/Support/Tree.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// A Tree data type with a `value` of some type `E` and `children` subtrees.
public class Tree<E> {
public var value: E
public var children: [Tree<E>] = []
public weak var parent: Tree<E>?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for remembering to make this weak


public init(value: E) {
self.value = value
}

/// Add a child to the tree's children and set a weak reference from the child to the parent (`self`)
public func addChild(settingParentOf child: Tree) {
children.append(child)
child.parent = self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,6 @@ public extension AWSAPIPlugin {
return operation
}

func subscribe(toAnyModelType modelType: Model.Type,
subscriptionType: GraphQLSubscriptionType,
listener: GraphQLSubscriptionOperation<AnyModel>.EventListener?) ->
GraphQLSubscriptionOperation<AnyModel> {
let request = GraphQLRequest<AnyModel>.subscription(toAnyModelType: modelType,
subscriptionType: subscriptionType)

let operationRequest = getOperationRequest(request: request,
operationType: .subscription)

let operation = AWSGraphQLSubscriptionOperation(
request: operationRequest,
pluginConfig: pluginConfig,
subscriptionConnectionFactory: subscriptionConnectionFactory,
authService: authService,
listener: listener)

queue.addOperation(operation)
return operation
}

private func getOperationRequest<R: Decodable>(request: GraphQLRequest<R>,
operationType: GraphQLOperationType) -> GraphQLOperationRequest<R> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ extension AWSAPIPlugin {

private func determineHostName(apiName: String?) throws -> String {
if let apiName = apiName {
guard let baseUrl = self.pluginConfig.endpoints[apiName]?.baseURL,
guard let baseUrl = pluginConfig.endpoints[apiName]?.baseURL,
let host = baseUrl.host else {
let error = APIError.invalidConfiguration("Invalid endpoint configuration for \(apiName)",
"""
Expand All @@ -59,7 +59,7 @@ extension AWSAPIPlugin {
throw error
}

guard let configEntry = self.pluginConfig.endpoints.first else {
guard let configEntry = pluginConfig.endpoints.first else {
let error = APIError.invalidConfiguration("No API configurations found",
"""
Review how the API category is being instantiated and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class AnyModelIntegrationTests: XCTestCase {

let callbackInvoked = expectation(description: "Callback invoked")
var responseFromOperation: GraphQLResponse<AnyModel>?
_ = Amplify.API.mutate(ofAnyModel: anyPost, type: .create) { response in
_ = Amplify.API.mutate(of: anyPost, type: .create) { response in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we discussed at some point removing the of: so the API would be API.mutate(anyPost, type: .create) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes, i still have those captured in the previous PR: https://github.com/aws-amplify/amplify-ios/blob/7db239ddb6e303fb9dad876033286f0c0ca8979a/Amplify/Categories/API/ClientBehavior/AmplifyAPICategory%2BGraphQLBehavior.swift

but would opt to do any of these in another PR. The change you see here is due to removing the mutate(ofAnyModel) API from Amplify.API. this set of test is disabled at the moment so i'm not sure if that's even the right call for the test. datastore uses the GraphQLRequest extensions so AnyModelIntegrationTests should actualy be removed. we test the sync/anymodel APIs in GraphQLSyncBasedIntegrationTests

defer {
callbackInvoked.fulfill()
}
Expand Down Expand Up @@ -101,7 +101,7 @@ class AnyModelIntegrationTests: XCTestCase {
let originalAnyPost = try originalPost.eraseToAnyModel()

let createCallbackInvoked = expectation(description: "Create callback invoked")
_ = Amplify.API.mutate(ofAnyModel: originalAnyPost, type: .create) { _ in
_ = Amplify.API.mutate(of: originalAnyPost, type: .create) { _ in
createCallbackInvoked.fulfill()
}

Expand All @@ -115,7 +115,7 @@ class AnyModelIntegrationTests: XCTestCase {

let updateCallbackInvoked = expectation(description: "Update callback invoked")
var responseFromOperation: GraphQLResponse<AnyModel>?
_ = Amplify.API.mutate(ofAnyModel: updatedAnyPost, type: .update) { response in
_ = Amplify.API.mutate(of: updatedAnyPost, type: .update) { response in
defer {
updateCallbackInvoked.fulfill()
}
Expand Down Expand Up @@ -164,7 +164,7 @@ class AnyModelIntegrationTests: XCTestCase {
let originalAnyPost = try originalPost.eraseToAnyModel()

let createCallbackInvoked = expectation(description: "Create callback invoked")
_ = Amplify.API.mutate(ofAnyModel: originalAnyPost, type: .create) { _ in
_ = Amplify.API.mutate(of: originalAnyPost, type: .create) { _ in
createCallbackInvoked.fulfill()
}

Expand All @@ -174,7 +174,7 @@ class AnyModelIntegrationTests: XCTestCase {

let deleteCallbackInvoked = expectation(description: "Delete callback invoked")
var responseFromOperation: GraphQLResponse<AnyModel>?
_ = Amplify.API.mutate(ofAnyModel: originalAnyPost, type: .delete) { response in
_ = Amplify.API.mutate(of: originalAnyPost, type: .delete) { response in
defer {
deleteCallbackInvoked.fulfill()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,15 @@ class GraphQLSyncBasedTests: XCTestCase {

let updatedTitle = title + "Updated"

let modifiedPost = Post(id: createdPost.model.id,
let modifiedPost = Post(id: createdPost.model["id"] as? String ?? "",
title: updatedTitle,
content: createdPost.model.content,
content: createdPost.model["content"] as? String ?? "",
createdAt: Date())

let completeInvoked = expectation(description: "request completed")
var responseFromOperation: GraphQLResponse<MutationSync<AnyModel>>?
let document = GraphQLSyncMutation(of: modifiedPost, type: .update, version: 1)
let request = GraphQLRequest(document: document.stringValue,
variables: document.variables,
responseType: MutationSync<AnyModel>.self,
decodePath: document.decodePath)

let request = GraphQLRequest<MutationSyncResult>.updateMutation(of: modifiedPost, version: 1)

_ = Amplify.API.mutate(request: request) { event in
defer {
Expand Down Expand Up @@ -107,7 +104,7 @@ class GraphQLSyncBasedTests: XCTestCase {
}

XCTAssertEqual(mutationSync.model["title"] as? String, updatedTitle)
XCTAssertEqual(mutationSync.model["content"] as? String, createdPost.model.content)
XCTAssertEqual(mutationSync.model["content"] as? String, createdPost.model["content"] as? String)
XCTAssertEqual(mutationSync.syncMetadata.version, 2)
}

Expand All @@ -132,11 +129,10 @@ class GraphQLSyncBasedTests: XCTestCase {
var responseFromOperation: GraphQLResponse<PaginatedList<AnyModel>>?
let post = Post.keys
let predicate = post.title == title
let document = GraphQLSyncQuery(from: Post.self, predicate: predicate, limit: 1, lastSync: 123)
let request = GraphQLRequest(document: document.stringValue,
variables: document.variables,
responseType: PaginatedList<AnyModel>.self,
decodePath: document.decodePath)
let request = GraphQLRequest<SyncQueryResult>.syncQuery(modelType: Post.self,
where: predicate,
limit: 1,
lastSync: 123)

_ = Amplify.API.query(request: request) { event in
defer {
Expand Down Expand Up @@ -198,12 +194,8 @@ class GraphQLSyncBasedTests: XCTestCase {
let disconnectedInvoked = expectation(description: "Connection disconnected")
let completedInvoked = expectation(description: "Completed invoked")
let progressInvoked = expectation(description: "Progress invoked")
let request = GraphQLRequest<MutationSyncResult>.subscription(to: Post.self, subscriptionType: .onCreate)

let document = GraphQLSubscription(of: Post.self, type: .onCreate, syncEnabled: true)
let request = GraphQLRequest(document: document.stringValue,
variables: document.variables,
responseType: MutationSync<AnyModel>.self,
decodePath: document.decodePath)
let operation = Amplify.API.subscribe(request: request) { event in
switch event {
case .inProcess(let graphQLResponse):
Expand Down Expand Up @@ -252,20 +244,16 @@ class GraphQLSyncBasedTests: XCTestCase {

// MARK: Helpers

func createPost(id: String, title: String) -> MutationSync<AmplifyTestCommon.Post>? {
func createPost(id: String, title: String) -> MutationSyncResult? {
let post = Post(id: id, title: title, content: "content", createdAt: Date())
return createPost(post: post)
}

func createPost(post: AmplifyTestCommon.Post) -> MutationSync<AmplifyTestCommon.Post>? {
var result: MutationSync<AmplifyTestCommon.Post>?
func createPost(post: AmplifyTestCommon.Post) -> MutationSyncResult? {
var result: MutationSyncResult?
let completeInvoked = expectation(description: "request completed")

let document = GraphQLSyncMutation(of: post, type: .create)
let request = GraphQLRequest(document: document.stringValue,
variables: document.variables,
responseType: MutationSync<AmplifyTestCommon.Post>.self,
decodePath: document.decodePath)
let request = GraphQLRequest<MutationSyncResult>.createMutation(of: post)
_ = Amplify.API.mutate(request: request, listener: { event in
switch event {
case .completed(let data):
Expand All @@ -285,5 +273,4 @@ class GraphQLSyncBasedTests: XCTestCase {
wait(for: [completeInvoked], timeout: TestCommonConstants.networkTimeout)
return result
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation

/// Adds conflict resolution information onto the document based on the operation type (query or mutation)
/// All selection sets are decorated with conflict resolution fields and inputs are added based on the values that it
/// was instantiated with. If `version` is passed, the input with key "input" will contain "_version" with the `version`
/// value. If `lastSync` is passed, the input will contain new key "lastSync" with the `lastSync` value.
public struct ConflictResolutionDecorator: ModelBasedGraphQLDocumentDecorator {

private let version: Int?
private let lastSync: Int?

public init(version: Int? = nil, lastSync: Int? = nil) {
self.version = version
self.lastSync = lastSync
}

public func decorate(_ document: SingleDirectiveGraphQLDocument,
modelType: Model.Type) -> SingleDirectiveGraphQLDocument {
var inputs = document.inputs

if let version = version,
case .mutation = document.operationType,
var input = inputs["input"],
case var .object(value) = input.value {

value["_version"] = version
input.value = .object(value)
inputs["input"] = input
}

if let lastSync = lastSync, case .query = document.operationType {
inputs["lastSync"] = GraphQLDocumentInput(type: "AWSTimestamp", value: .scalar(lastSync))
}

if let selectionSet = document.selectionSet {
addConflictResolution(selectionSet: selectionSet)
return document.copy(inputs: inputs, selectionSet: selectionSet)
}

return document.copy(inputs: inputs)
}

/// Append the correct conflict resolution fields for `model` and `pagination` selection sets.
private func addConflictResolution(selectionSet: SelectionSet) {
switch selectionSet.value.fieldType {
case .value:
break
case .model:
selectionSet.addChild(settingParentOf: .init(value: .init(name: "_version", fieldType: .value)))
selectionSet.addChild(settingParentOf: .init(value: .init(name: "_deleted", fieldType: .value)))
selectionSet.addChild(settingParentOf: .init(value: .init(name: "_lastChangedAt", fieldType: .value)))
case .pagination:
selectionSet.addChild(settingParentOf: .init(value: .init(name: "startedAt", fieldType: .value)))
}

selectionSet.children.forEach { child in
addConflictResolution(selectionSet: child)
}
}
}
Loading