-
Notifications
You must be signed in to change notification settings - Fork 199
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
Conversation
12a9711
to
b6060f7
Compare
b6060f7
to
31298c1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're getting much closer. I have some concerns about a few things that I called out. In particular, I'd like to understand Selection Set handling better.
|
||
/// Holds all of the possible operations such as `.get`, `.list`, `.sync`, `create`, `update`, `delete`, `onCreate`, | ||
/// `onUpdate`, `onDelete` defined, for each GraphQL operation type (query, mutation, subscription) | ||
enum GraphQLOperationSubType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- What's the difference between an "operation type" and an "operation subtype"? Why is a "subtype" hierarchy in the DirectiveDecorator as opposed to being declared & managed with the "super type"?
- Don't we have one or two representations of this concept already? I'm not sure exactly where, but I remember having to translate "mutation" between a couple of contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mainly used this to as a private property on DirectiveDecorator to hold one of the values of GraphQLQueryType
, GraphQLSubscriptionType
, or GraphQLMutationType
. Since I'm just exposing 3 separate constructors for the DirectiveNameDecorator, this type can actually be removed completely.
ie. caller can only do one of the following anyways:
public init(queryType: GraphQLQueryType)
public init(mutationType: GraphQLMutationType)
public init(subscriptionType: GraphQLSubscriptionType)
AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/DirectiveDecorator.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/ModelDecorator.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLDocument/GraphQLQuery.swift
Outdated
Show resolved
Hide resolved
name: String, | ||
inputs: [GraphQLParameterName: GraphQLDocumentInput], | ||
selectionSet: SelectionSet?) { | ||
self.operationType = operationType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and above: do we need to init with an OperationType if we're creating a GraphQLSubscription ? Wouldn't the OperationType be inferrable in this init?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reason why there is an all parameter init is for the deep copy method created for the Decorators to return a new instance of the document after decorating
public func copy(operationType: GraphQLOperationType? = nil,
name: String? = nil,
inputs: [GraphQLParameterName: GraphQLDocumentInput]? = nil,
selectionSet: SelectionSet? = nil) -> Self {
return Self.init(operationType: operationType ?? self.operationType,
name: name ?? self.name,
inputs: inputs ?? self.inputs,
selectionSet: selectionSet ?? self.selectionSet)
}
var graphQLName: String { | ||
name | ||
/// The GraphQL directive name translated from the GraphQL operation and model schema data | ||
func graphQLName(operationSubType: GraphQLOperationSubType) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like an appropriate use of an extension: taking a custom type and extending it with more custom behavior that doesn't necessarily represent the core functionality of the type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ended up removing GraphQLOperationSubType
and creating three extensions with parameters taking in GraphQLQueryType'
GraphQLMutationType, etc
AmplifyPlugins/Core/AWSPluginsCore/Model/Support/ModelSchema+GraphQL.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/Core/AWSPluginsCore/Model/Support/SelectionSet.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/Core/AWSPluginsCore/Model/Support/SelectionSet.swift
Outdated
Show resolved
Hide resolved
let request = GraphQLRequest<SyncQueryResult>.syncQuery(modelType: modelType, | ||
nextToken: nextToken, | ||
lastSync: lastSyncTime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much nicer!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
@@ -30,4 +30,9 @@ extension String { | |||
public func camelCased() -> String { | |||
return prefix(1).lowercased() + dropFirst() | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename file since it's no longer just "casing" :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updating to String+Extensions
(hmm, couldn't think of something better lol)
@@ -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 |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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
AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/ConflictResolutionDecorator.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/ModelDecorator.swift
Outdated
Show resolved
Hide resolved
@@ -98,7 +98,7 @@ extension GraphQLRequest { | |||
return GraphQLRequest<[M]>(document: document.stringValue, | |||
variables: document.variables, | |||
responseType: [M].self, | |||
decodePath: document.name) | |||
decodePath: document.name + ".items") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a backlog item to refactor decode path handling. It doesn't make sense for the query function at this level to know the details of pagination implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have this bug here for fixing the model based list query API #291
@@ -10,7 +10,7 @@ import Foundation | |||
/// A container to hold either an object or a value, useful for storing document inputs and allowing manipulation at | |||
/// the first level of the object | |||
public enum GraphQLDocumentInputValue { | |||
case value(Any) | |||
case scalarOrString(Any) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above: scalar
should be sufficient--all non-optional types can be safely interpolated via String(describing:)
. However, what you really want is for the incoming value to be safely representable as a primitive type (as opposed to a nested JSON-like object), rather than relying on an internal String(describing:)
implementation that may change without warning (hello, NSData!) so you could define some protocol, and some convenience extensions to make it trivial to pass primitives into the enum value:
public enum GraphQLDocumentInputValue {
case value(GraphQLDocumentValueRepresentable)
case object([String: GraphQLDocumentValueRepresentable])
}
public protocol GraphQLDocumentValueRepresentable {
var graphQLDocumentValue: String { get }
}
extension Bool: GraphQLDocumentValueRepresentable {
public var graphQLDocumentValue: String {
return "\(self)"
}
}
extension Float: GraphQLDocumentValueRepresentable {
public var graphQLDocumentValue: String {
return "\(self)"
}
}
extension Int: GraphQLDocumentValueRepresentable {
public var graphQLDocumentValue: String {
return "\(self)"
}
}
extension String: GraphQLDocumentValueRepresentable {
public var graphQLDocumentValue: String {
return self
}
}
...etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for providing the example! i added this in with a few modifications:
- Added
extension Decimal: GraphQLDocumentValueRepresentable
as we represent Double's as Decimal. from what i recall it something to do with rounding. current we don't have any scalar cases other than int and string, but left this in for now. case object([String: GraphQLDocumentValueRepresentable])
doesn't quite work since if you takeQueryPredicate
to GraphQL input for example, the value can be infinitely nested. so the.object
case is something recursive like
public enum GraphQLDocumentInputValue {
case scalar(GraphQLDocumentValueRepresentable)
case object([String: GraphQLDocumentInputValue])
}
currently still have it as [String: Any?] and will see if the above works out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so digging into this deeper, having .scalar(GraphQLDocumentValueRepresentable)
seems to work well in this class. Adding it to the .object[String:GraphQLDocumentValueRepresentable]
case doesn't work as it may contain arrays as well. then it will evolve to something similar to JSONValue
.
public struct SelectionSet { | ||
var fields: [SelectionSetField] | ||
var type: SelectionSetType | ||
public class TreeNode<E> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Put
TreeNode
in its own file TreeNode
is a public class, but its methods and properties are internal. Intentional?- Since we're naming a type, we should probably call it
Tree
instead ofTreeNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updating 1 and 3, for 2, Tree
is pubic for the typealias SelectionSet
as SelectionSet is used as a property on the SingleDirectiveGraphQLDocument
public protocol. Everything else on the Tree is used internally.
} | ||
|
||
mutating func append(_ selectionSetField: SelectionSetField) { | ||
fields.append(selectionSetField) | ||
func add(child: TreeNode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has the non-obvious side-effect of mutating the child. Suggest you rename the method to make it more obvious to addChild(settingParentOf child:)
or similar, and add a doc comment with a note. It's the right behavior--it's would be cumbersome to require call sites to do this explicitly--but we just want to make sure that it's obvious what's happening when we call the method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, updating! can go with your suggestion. I was also thinking of insert(settingParentOf child:)
- some remaining code clean up
eb0624d
to
f3082c2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question about Decimal, but otherwise LGTM
public class Tree<E> { | ||
public var value: E | ||
public var children: [Tree<E>] = [] | ||
public weak var parent: Tree<E>? |
There was a problem hiding this comment.
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(type: String, value: GraphQLDocumentInputValue) { | ||
self.type = type | ||
self.value = value | ||
extension Decimal: GraphQLDocumentValueRepresentable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised we're using Decimal--I thought we had moved over to Float for our codegenned models? Do we at least need to add a conformance for Float, since it's a standard library, as opposed to a Foundation, type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we landed on Double because it can hold up to 64 bit. then i encountered some serialization issues, a Double with value 12.3 is serialized as "12.300000000001", which is why i started using Decimal (#159). so still not 100% confident in which is the right type to use as looking up more details on Decimal isn't giving me many results to resolve this. Maybe there to use Double and override the serialization logic on Doubles. I think the best test is to write an integration test which has a single model with all of the various types that we support.
Float in AppSync is currently returning .double as the ModelFieldType, which then gets translated to a Decimal and serialized. https://github.com/aws-amplify/amplify-ios/blob/461c3cfd8923fbbb6eb10b60495b0f4594879cf4/Amplify/Categories/DataStore/Model/ModelSchema%2BDefinition.swift#L56
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opening an issue track adding test with all types that are supported, may be completed as part of @drochetti 's work or follow up task #314
This adds a GraphQLDocument Builder class which holds a list of decorators for intercepting the document to decorate it with graphql related components using Model related data
list of decorators
Usage
The currently split between APIPlugin's use of GraphQLRequest and DataStore's are implemented in the GraphQLRequest extensions, I tried not to make many changes here other than consolidating the methods places which these methods live
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.