-
Notifications
You must be signed in to change notification settings - Fork 205
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
include associations in the GraphQL query selected fields #164
Conversation
**Notes:** - added logic to handle associations when generating GraphQL documents for Queries and Subscriptions - fixed value resolution for associations - added some tests to check the output of associated models queries
@@ -14,6 +14,10 @@ extension LoggingCategory: LoggingCategoryClientBehavior { | |||
plugin.logger(forCategory: category) | |||
} | |||
|
|||
public func logger(forCategory category: CategoryType) -> Logger { |
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.
@palpatim would like to get your thoughts on this
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 had a similar thought. From #149:
/// Defines a `log` convenience property, and provides a default implementation that returns a Logger for a category
/// name of `String(describing: self)`
public protocol DefaultLogger {
static var log: Logger { get }
var log: Logger { get }
}
public extension DefaultLogger {
static var log: Logger {
Amplify.Logging.logger(forCategory: String(describing: self))
}
var log: Logger {
type(of: self).log
}
}
Then any type can get its own type-specific logger by declaring a no-op conformance to DefaultLogger
.
Note that the current Logging subsystem is broken for types that want to use it during initialization, or during configuration in tests after an Amplify.reset()
. Tracking that on #161.
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.
Of course now that I look at my implementation, I realize I added DefaultLogger
in AWSPluginsCore
rather than AmplifyCore
. sigh I'll move it to AmplifyCore in an upcoming PR, and ensure that each category also conforms.
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.
Cool, thanks for the context. I'll rely on your changes for then
fieldSet.append(indent + field.graphQLName) | ||
} | ||
} | ||
fieldSet.append(indent + "__typename") |
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.
@palpatim is it safe to assume we should add __typename
for queries and subscriptions (mutations override this logic)?
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.
@drochetti Yes, I believe that is a safe assumption. Mutations will need the field as well, but I assume you're referring to overriding the assigment of __typename
during the AnyModel flow?
id | ||
content | ||
createdAt | ||
postId |
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.
point-of-interest: in mutations, the associated id (aka "foreign key") is passed
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 i'm discovering right now is that the selectionSet should be the same for both mutations and queries in this case. the CreateComment can also return the same thing as GetComment.
mutation CreateComment($input:CreateCommentInput!){
createComment(input: $input) {
id
content
createdAt
post {
id
_version
content
createdAt
draft
rating
title
updatedAt
__typename
}
}
}
{
"input": {
"content": "Content",
"createdAt": "2019-11-25T00:35:01.746Z",
"commentPostId": "123"
}
}
as in, the response for a CreateComment mutation is the same object as the response for a GetComment. this being the Post is eager loaded.
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 question is, should the selection set be different on Mutations over Queries?
Let's take a look at if Post is required field on Comment and selection set does not specify the Post
mutation CreateComment($input:CreateCommentInput!){
createComment(input: $input) {
id
content
createdAt
}
}
{
"input": {
"content": "Content",
"createdAt": "2019-11-25T00:35:01.746Z",
"commentPostId": "123"
}
}
The Comment Model contains a required Post and data does not come back. this means it will fail to deserialize.
scenario 2a: Post is not a required field on the Comment, and selection set contains the Post. Deserialized Comment will contain post.
scenario 2b: Post is not required field on the Comment, and selection set does not contain Post. Deserialized Comment contains nil post
field, which is fine.
id | ||
content | ||
createdAt | ||
post { |
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.
point-of-interest: in queries (and subscriptions) the required associations are added to the selection set (otherwise decoding would fail). In the future we should discuss giving the control to users define which associates are included or not (eager
vs lazy
loading).
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.
"required associations are added" to what depth?
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.
That's a good question and the part I struggled with. Right now the depth is not specified. This logic will even break in case or circular references.
How to decode missing properties to a Struct that require them? I'm having a hard time keeping the data model types consistent but optimize for performance and different use cases of associations (lazy vs eager, required vs optional)
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 codegen specifies a limit of 2; we can take that as a guideline.
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 created #168 to track it. I'll also look into @propertyWrapper
. I've played with it when I was trying to define the "schema" definition without much success, but now I have more context on how it works, I might be successful in that endeavor.
@@ -14,6 +14,10 @@ extension LoggingCategory: LoggingCategoryClientBehavior { | |||
plugin.logger(forCategory: category) | |||
} | |||
|
|||
public func logger(forCategory category: CategoryType) -> Logger { |
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 had a similar thought. From #149:
/// Defines a `log` convenience property, and provides a default implementation that returns a Logger for a category
/// name of `String(describing: self)`
public protocol DefaultLogger {
static var log: Logger { get }
var log: Logger { get }
}
public extension DefaultLogger {
static var log: Logger {
Amplify.Logging.logger(forCategory: String(describing: self))
}
var log: Logger {
type(of: self).log
}
}
Then any type can get its own type-specific logger by declaring a no-op conformance to DefaultLogger
.
Note that the current Logging subsystem is broken for types that want to use it during initialization, or during configuration in tests after an Amplify.reset()
. Tracking that on #161.
fieldSet.append(indent + field.graphQLName) | ||
} | ||
} | ||
fieldSet.append(indent + "__typename") |
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.
@drochetti Yes, I believe that is a safe assumption. Mutations will need the field as well, but I assume you're referring to overriding the assigment of __typename
during the AnyModel flow?
// All mutation documents should include typename, to support type-erased operations on the client | ||
fields.append("__typename") | ||
|
||
let fields = selectionSet |
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.
probably safe to remove this and just interpolate selectionSet
below--not sure the variable adds clarity any more
/// - Note: Currently implementation assumes the most common and efficient queries. | ||
/// Future APIs might allow user customization of the selected fields. | ||
public var selectionSet: [String] { | ||
var fieldSet = [String].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.
- Avoid appending type information to variable names, except in cases where it is needed to disambiguate, e.g. during a transfomation. In this case, it's actively misleading since it's an Array, not a Set. :) I think the intent of the field is to store components of the selection set, so maybe
selectionSetFields
or similar? - Avoid explicit calls to
.init
, prefer initializers likevar foo = [String]()
|
||
var indentSize = 0 | ||
|
||
func appendFields(_ fields: [ModelField]) { |
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.
Why a nested function? Seems like this could be a standalone static function
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.
To make it a standalone function more work would be necessary to handle the recursive nature of the data structure. It seems that it would also require an inout
argument.
I find that nested functions are a good fit for recursive logic. That or this is just my inner JavaScript side speaking
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.
OK, I didn't trace through the flow in depth, so I'm happy to use nested functions as well. Us old JS hands gotta stick together. :)
/// | ||
/// - Note: Currently implementation assumes the most common and efficient queries. | ||
/// Future APIs might allow user customization of the selected fields. | ||
public var selectionSet: [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.
- Why is this property public?
- Why are you composing a formatted string, but returning the individual components instead of the final output?
- The name
selectionSet
is also on the model, but that property which returns a list of field names, thus adding to the confusion.
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.
- Good call, no need
- Yeah, I initially thought about returning an array of fields. But then struggled with nested properties. Then decided to return the formatted string, but then struggled with the final formatting of the document, since I don't know the indentation level of the document at this point.
- I could make this a function as take parameters in order to produce the correct string output
- or I could return with no indentation and the consuming code adds a padding to each line? Argh...
- True, good call. I'll change.
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.
Yeah, formatting is tricky. I think if you rename the property to "selectionSetComponents" or similar, and let the final document composer handle indentation, I'd find it eaiser to understand
id | ||
content | ||
createdAt | ||
post { |
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.
"required associations are added" to what depth?
query GetComment($id: ID!) { | ||
getComment(id: $id) { | ||
id | ||
content | ||
createdAt | ||
post { | ||
id | ||
_version | ||
content | ||
createdAt | ||
draft | ||
rating | ||
title | ||
updatedAt | ||
__typename | ||
} | ||
__typename |
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 issue i'm seeing for decoding Post exists here as well. Post contains List.
In this code, the response data from the service will then deserialize the data into a Comment which contains a Post and will fail to find the comments since we are not asking for it back in the selection set. List's decoder is never called. This is the same failure in a simpler case when we call Mutation CreatePost or Query GetPost. Both will return a Post data which we try to deserialize into the Post Model.
So we need to add to the selection set "comments"
Here's an example of my attempt in the AppSync console:
To do this, let's add "comments" in the selection set
mutation CreatePost($input:CreatePostInput!) {
createPost(input: $input) {
id
title
comments //this does not work, it needs at least one selection for comments
}
}
This works:
mutation CreatePost($input:CreatePostInput!) {
createPost(input: $input) {
id
title
comments {
//items { // commented out as an example, that, in the future if we support user defined control, then we will probably generate the items with the selection set for Comment as well
// id
//}
nextToken
}
}
}
returns:
{
"data": {
"createPost": {
"id": "84b1a5ca-7ead-48da-9b65-8df458d1f613",
"title": "title",
"comments": {
"nextToken": null
}
}
}
}
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.
some options
-
setting Post's List to optional like
comments: List<Comment>?
does work for decoding since the key does not exist in the response and it is not required in the Model so decoder is happy. Model gen needs to be updated. also datastore local store logic. developer experience is poor since they have to now check that the Post that is return contains comments that is not nil before operating on the List -
Always set selection set to contain
comments
withnextToken
, and in the future when we support user defined control,items
gets added to thecomments
. This does eager loading but does not return any results from the service. i do not think this is what we want just to make decoder happy. -
somehow, customize the decoding logic for the Post Model which contains comments that is a List. when decoding, when key does not exist for a required field like
comments
, we want to create List() that is just empty
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 was hopping that List<ModelType>
decoder would be called with nil
, but since that's not the case (bummer) I would say let's go with #1
. Make it optional and call it a day.
@lawmicha thanks for testing it and providing all the context, appreciate it
case .model: | ||
input[name] = (value as? Model)?.id |
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 have updated this code to produce "commentPostId" and can take it forward
let fieldName = modelName.lowerCaseFirstLetter() + name
input[fieldName] = (value as? Model)?.id
/// - the subscription is of type `.onCreate` | ||
/// - Then: | ||
/// - check if the generated GraphQL document is a valid subscription | ||
/// - it has a list of fields with no nested/connected models | ||
/// - it has a list of fields with no nested models | ||
func testOnCreateGraphQLSubscriptionFromSimpleModel() { |
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.
no action needed, adding note here:
- onCreateComment should also return the Post like a CreateComment selection set
- will take a look up UpdateComment/DeleteComment, this scenario is important for Comment containing a Post.
- Will add integration testing around these
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.
LGTM. Retroactive Ship-It!
* chore: Updating to the renamed WebAuthn APIs * Fixing unit tests
* feat(auth): adding support for email mfa (#3892) * feat(auth): adding support for email mfa * fix swift lint warning * worked on a review comment * adding integration tests wave 1 * integration tests wave 2 * integration tests wave 3 * Add test setup instructions wave 4 * Add edge case * update readme to include graphQL details * chore: initial commit to add sdk with passwordless models * chore: model update * feat(Auth): Adding WebAuthn APIs (#153) * feat(Auth): Adding List WebAuthn API * feat(Auth): Adding associate and delete WebAuthn credentials APIs * Addding missing transports array in the credentials payload * Adding friendlyName to AuthWebAuthnCredential * Adding excludedCredentials to avoid multiple PassKeys for the same device * Adding pagination support in the list API * Renaming CredentialPayload to CredentialRegistrationPayload * Addressing PR comments * feat(auth): add passwordless sign with otp (#151) * feat(auth): add passwordless OTP implementation * add fallback password and password srp flows * add web auth n states * modifying states * feat(Auth): Adding WebAuthn support to signIn and confirmSignIn APIs (#155) * feat(auth): add passwordless OTP implementation * add fallback password and password srp flows * add web auth n states * modifying states * feat(Auth): Implementing signIn with WebAuthn * Adding support for a presentation anchor in sign in and confirm sign in options * Fixing errors * Addressing PR comments * fix build error --------- Co-authored-by: Harshdeep Singh <6162866+harsh62@users.noreply.github.com> * fix: Fixing build issue when iOS 18/macOS 15 are not installed * feat(WebAuthn): Adding support for retrying a confirmSignIn with WebAuthn request, if the first one fails (#158) * feat(auth): add support for passwordless sign up and auto sign in (#160) * add autoSignIn() category API definitions (#152) * add autoSignIn() category API definitions * add sign up step for auto sign in * add state machine changes for autoSignIn() and signUp() (#154) * add autoSignIn() category API definitions * add sign up step for auto sign in * add state machine changes * add events and update resolvers * update sign up events and resolvers * add updates to resolver for auto sign in * update confirm sign up flow and debug code * Address review comments --------- Co-authored-by: Harsh <6162866+harsh62@users.noreply.github.com> * update auto sign state machine events and resolver (#157) * update auto sign state machine events and resolver * Address review comments * update sign up and auto sign in unit tests (#159) * update sign up and auto sign in unit tests * add auto sign in tests and refactor existing tests * Add more service error tests * Address review changes --------- Co-authored-by: Harsh <6162866+harsh62@users.noreply.github.com> * chore: fix building of unit tests after sign up rebase * feat(auth): adding passwordless sign in preferred flows (#162) * feat(auth): add passwordless preferred flow * adding confirm device and device srp flows to user auth * update message * worked on review comments * update * chore(auth): add more auto sign in and sign up state machine/e2e unit tests (#161) * chore(auth): add more auto sign in and sign up state machine/e2e unit tests * Address review comments * chore: updated SDK and models * chore: update integration test host app * fix: Fixing build errors in watchOS/tvOS due to missing prechecks. * feat(auth): adding an initial passwordless integration test with resources defined (#163) * chore: update no-auth API's in the resolver * chore: Updating to the renamed WebAuthn APIs (#164) * chore: Updating to the renamed WebAuthn APIs * Fixing unit tests * chore: Adding unit tests for the WebAuthn APIs Tasks (#165) * test: Adding AssociateWebAuthn unit tests * test: Adding ListWebAuthnCredentials unit tests * test: Adding DeleteWebAuthnCredential unit tests * chore: simplifying how webauthn errors are handled * adressing PR comments * chore(auth): add integration tests for passwordless signup and auto sign in (#166) * chore(auth): add integration tests for passwordless signup and auto sign in * remove unused code * refactor code * chore: add integration tests for sign in flows (#168) * chore: add integration tests for sign in flows * Update AuthSignInWithPasswordUsingUserAuthTests.swift * Add more integration tests * update * chore: update sdk to use the latest models * test: Adding integration tests for WebAuthn APIs (#169) * test: Adding integration tests for WebAuthn APIs * chore: Adding webauthn integration workflow * Refactoring the code to remove unnecesary waits and make it more easy to read * fix: Fixing service errors being reported as .unknown when sign in fails (#170) * fix: Fixing service errors being reported as .unknown when sign in fails. Also adding proper WebAuthn cases to the AWSCognitoAuthError enum. * addressing PR comment * fix(auth): fix resolvers and tasks for auto sign in when state machine is in signing in state (#172) * fix(auth): fix resolvers and tasks for auto sign in when state machine is in signin in state * fix indentation * feat: Adding visionOS support to the WebAuthn APIs (#171) * chore: using the latest version aws sdk * chore: update changes needed for the sdk updated * chore: fix swiftlint errors (#3921) * chore: running passwordless integration tests on GEN2 backend * chore: update test target for watchOS * chore: fix OTP integration tests * chore: update more integration tests --------- Co-authored-by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Co-authored-by: Abhash Kumar Singh <thisisabhash@gmail.com>
Notes:
for Queries and Subscriptions
Issue #, if available:
Description of changes:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.