amplify-swift icon indicating copy to clipboard operation
amplify-swift copied to clipboard

How do I query on a @key (Global Secondary Index)?

Open bumbleparrot opened this issue 4 years ago • 7 comments

Describe the bug I have used the Amplify CLI to generate my GraphQL schema. The query and Schema work in the AWS AppSync Console. The DynamoDB tables are created and the query returns data via the online console.

My issue is that I have no idea how to query beyond the basic ".get(byId: [$id]" method via Amplify.API.query() inside of my iOS app using the Amplify Library.

To Reproduce Below is my schema. ` type Profile @model { id: ID! firstName: String lastName: String email: String socialProviderId: String socialProvider: String verified: Boolean address: [Address] @connection(name: "address", fields: ["id"]) incident: [Incident] @connection(name: "incident", fields: ["id"]) camera: [Camera] @connection(name: "camera", fields: ["id"]) }

type Address @model @key(fields: ["id", "streetNumber"]) @key(name:"uniqueCode-index", fields:["uniqueCode"], queryField: "addressUniqueCode" ) @key(name:"geohashFirst6-index", fields:["geohashFirst6"], queryField: "addressGeohashFirst6" ) @key(name:"geohashLast5-index", fields:["geohashLast5"], queryField: "addressgGohashLast5" ) @key(name:"geohashLast4-index", fields:["geohashLast4"], queryField: "addressGeohashLast4" ) @key(name:"geohashLast3-index", fields:["geohashLast3"], queryField: "addressGeohashLast3" ) @key(name:"geohashLast2-index", fields:["geohashLast2"], queryField: "addressGeohashLast2" ) @key(name:"geohashLast1-index", fields:["geohashLast1"], queryField: "addressGeohashLast1" ) @key(name:"geohash-index", fields:["geohash"], queryField: "addressGeohash" ) @key(name:"latitude-index", fields:["latitude"], queryField: "addressLatitude" ) @key(name:"longitude-index", fields:["longitude"], queryField: "addressLongitude" ) { id: ID! createdAt: String geohash: String geohashFirst6: String geohashLast1: String geohashLast2: String geohashLast3: String geohashLast4: String geohashLast5: String geohashLast6: String latitude: Float longitude: Float state: String streetName: String streetNumber: Int! unit: String updatedAt: String zipCode: String cameras: [Camera] @connection(name: "cameras", fields: ["id"]) claimed: Boolean userId: Profile @connection(name: "claimedBy", fields: ["id"]) uniqueCode: String }

type Camera @model @key(fields: ["id"]) @key(name:"type-index", fields:["type"], queryField: "type" ) { id: ID! name: String type: String direction: String address: Address @connection(name: "atAddress", fields: ["id"]) }

type Incident @model @key(fields: ["id"]) @key(name:"geohash-index", fields:["geohash"], queryField: "incidentGeohash" ) @key(name:"geohashFirst6-index", fields:["geohashFirst6"], queryField: "incidentGeohashFirst6" ) @key(name:"geohashLast5-index", fields:["geohashLast5"], queryField: "incidentGeohashLast5" ) @key(name:"geohashLast4-index", fields:["geohashLast4"], queryField: "incidentGeohashLast4" ) @key(name:"geohashLast3-index", fields:["geohashLast3"], queryField: "incidentGeohashLast3" ) @key(name:"geohashLast2-index", fields:["geohashLast2"], queryField: "incidentGeohashLast2" ) @key(name:"geohashLast1-index", fields:["geohashLast1"], queryField: "incidentGeohashLast1" ) @key(name:"latitude-index", fields:["latitude"], queryField: "incidentLatitude" ) @key(name:"longitude-index", fields:["longitude"], queryField: "incidentLongitude" ) { id: ID! dateTime: AWSDateTime geohash: String geohashFirst6: String geohashLast1: String geohashLast2: String geohashLast3: String geohashLast4: String geohashLast5: String geohashLast6: String latitude: Float longitude: Float title: String description: String userId: Profile @connection(name: "reportedBy", fields: ["id"]) } `

Expected behavior I should be able to query this without any issue right?

Code I based my code on this example in the documents because I can't figure out any other obvious way to query the indexes i created in the schema.

Here is my code. that starts the Amplify.API.request Amplify.API.query(request: .getAddressByUniqueCode(uniqueCode: pinCode)) { response in switch(response) { case .success(let result): switch(result) { case .success(let address): print("Address: \(address)") break case .failure(_): print("getById - error: Error in result") break } break case .failure(_): print("getById - error: Error in response") break }

Here is how i define .getAddressByUniqueCode ` extension GraphQLRequest { static func getAddressByUniqueCode(uniqueCode uniqueCode: String) -> GraphQLRequest<Address> { let operationName = "addressUniqueCode" let document = """ query addressUniqueCode($uniqueCode: String) { (operationName)(uniqueCode: $uniqueCode) { nextToken items { geohashFirst6 streetName streetNumber longitude state latitude id zipCode updatedAt unit uniqueCode geohashLast6 geohashLast5 geohashLast4 geohashLast3 geohashLast2 geohashLast1 geohash createdAt claimed } } } """

    return GraphQLRequest<Address>(document: document,
                                variables: ["uniqueCode": uniqueCode],
                                responseType: Address.self,
                                decodePath: operationName)
}

} `

The error I am getting is: No value associated with key CodingKeys(stringValue: "id", intValue: nil) ("id").

Is there an issue with my schema or how I queried it? The same query works fine in the online AWS AppSync Console.

Environment(please complete the following information):

  • Amplify Framework Version: 4.29.7
  • Dependency Manager: Cocoapods
  • Swift Version : 5.0

Device Information (please complete the following information):

  • Device: iPhone 11 Simulator
  • iOS Version: iOS 14

Additional context Add any other context about the problem here.

bumbleparrot avatar Oct 16 '20 20:10 bumbleparrot

After some really really careful debugging. I got it to work by doing the following:

Update the code to my GraphQLRequest extension to the following: ` static func getAddressByUniqueCode(uniqueCode uniqueCode: String) -> GraphQLRequest<[Address]> { let operationName = "addressUniqueCode.items" let document = """ query addressUniqueCode($uniqueCode: String) { addressUniqueCode(uniqueCode: $uniqueCode) { nextToken items { geohash geohashFirst6 geohashLast1 id zipCode updatedAt unit uniqueCode streetNumber streetName state longitude latitude geohashLast6 geohashLast5 geohashLast4 geohashLast3 geohashLast2 createdAt claimed } } } """

    return GraphQLRequest<[Address]>(document: document,
                                variables: ["uniqueCode": uniqueCode],
                                responseType: **[Address].self**,
                                decodePath: operationName)
}

`

The main difference is is:

  1. Since this is a query on a secondary index, it will return a list. So I changed the "Address" to [Address].
  2. The decodePath is dot notated. So I updated the path to the list to be "addressUniqueCode.items" which results in the array to be stored in the responseType.

Now that this works, my new question is:

  1. Is this the correct pattern?
  2. Do I have to write my own extension and static function for each Dynamo DB Index I want to query via Amplify.API.query?

I have many other indexes I need to query and I want to ensure I'm doing it right.

bumbleparrot avatar Oct 17 '20 00:10 bumbleparrot

@drochetti , Hello! Do you happen to know if this is the right way to query on GSI's created through the schema.graphql and AWS Amplify CLI?

bumbleparrot avatar Oct 20 '20 16:10 bumbleparrot

Hi @bumbleparrot, as of now the way you did is the right way. We are evaluating a more automated way of providing those queries with the correct iOS types. But in the meantime, if you create your own extension like you did it should do it.

I'm happy to chat more about your use case, you said you have many more GSI queries and I understand creating your own extension for every single one is not ideal. Feel free to reach out to me on Discord or on Twitter, happy to chat more and gather your feedback while we address this pain point.

drochetti avatar Oct 22 '20 19:10 drochetti

Thank you @drochetti, that is helpful.

I think the GraphQL schema should generate code for all queries annotated by @connection and @key. We can use the attributes of @key and @connection to determine the method name as well as the input parameters and its type.

One of the challenges this introduces is how deep is the default nesting for the models that get returned? Theres no data in the schema that would indicate that.

bumbleparrot avatar Oct 29 '20 13:10 bumbleparrot

Hello @drochetti, hello @bumbleparrot

I am about to build my own application using deeply nested GraphQL queries, and I am encountering a Problem when decoding the Server response - maybe the issue is somewhat related to this one here.

Describe the bug I have created query by the help of the Mock API which I send to the GraphQL endpoint succesfully. The response is received as well, but cannot be decoded into the response type I have hoped to be the correct one.

To Reproduce

  • My Schema Definition

The model type UserChat is used to describe a many to many relationship between Users and Chats (I am in a rather early stage of development so the schema might offer room for improvement)

`type User @model @auth(rules: [{allow: owner}]) @key(name: "UserByMail", fields: ["mail"], queryField: "UserByMail") { id: ID! mail: String! password: String! firstName: String! lastName: String! gender: String! participatingChats: [UserChat] @connection(keyName: "UserChatByUser", fields: ["id"]) ownedChats: [Chat] @connection(keyName: "ChatByOwner", fields: ["id"]) messages: [Message] @connection(keyName: "MessageByUser", fields: ["id"]) }

type Chat @model @auth(rules: [{allow: owner, operations: [create, delete, update]}]) @key(name: "ChatByOwner", fields: ["ownerID"], queryField: "ChatByOwner") { id: ID! title: String! ownerID: ID! owner: User @connection(fields: ["ownerID"]) members: [UserChat] @connection(keyName: "UserChatByChat", fields: ["id"]) messages: [Message] @connection(keyName: "MessageByChat", fields: ["id"]) }

type UserChat @model(queries: null) @auth(rules: [{allow: owner}]) @key(name: "UserChatByChat", fields: ["chatID", "userID"], queryField: "UserChatByChat") @key(name: "UserChatByUser", fields: ["userID", "chatID"], queryField: "UserChatByUser") { id: ID! chatID: ID! chat: Chat! @connection(fields: ["chatID"]) userID: ID! user: User! @connection(fields: ["userID"]) }

type Message @model @auth(rules: [{allow: owner, operations: [create, delete, update]}]) @key(name: "MessageByChat", fields: ["chatID", "content"], queryField: "MessageByChat") @key(name: "MessageByUser", fields: ["userID", "content"], queryField: "MessageByUser") { id: ID! content: String! chatID: ID! chat: Chat @connection(fields: ["chatID"]) userID: ID! user: User @connection(fields: ["userID"]) }`

  • My GraphQLRequest Extension

` static func initialChatLoad(mail: String)-> GraphQLRequest<[User]>{ let operationName = "UserByMail" let document = """ query UserByMail($mail: String!) { (operationName)(mail: $mail) { items { firstName lastName gender id mail participatingChats { items { chatID id chat { id title ownerID messages { items { content chatID createdAt id owner } } } } } } } } """

    return GraphQLRequest<[User]>(document: document, variables: ["mail": mail], responseType: [User].self, decodePath: operationName)
}`
  • The Logs from the Console when executing the request

2021-10-24 11:57:40.761179+0200 app[86980:4209469] [API] Starting query D6A1ADB4-229D-4659-9FED-3DAEF4FE92A4 2021-10-24 11:57:40.761348+0200 app[86980:4209469] [API] { "variables" : { "mail" : "[email protected]" }, "query" : "query UserByMail($mail: String!) {\n UserByMail(mail: $mail) {\n items {\n firstName\n lastName\n gender\n id\n mail\n participatingChats {\n items {\n chatID\n id\n chat {\n id\n title\n ownerID\n messages {\n items {\n content\n chatID\n createdAt\n id\n owner\n }\n }\n }\n }\n }\n }\n }\n}" } 2021-10-24 11:57:40.807783+0200 app[86980:4209469] [API] Starting network task for query D6A1ADB4-229D-4659-9FED-3DAEF4FE92A4 Got failed result with Failed to decode GraphQL response to the ResponseType Array<User>

The ids and app name is changed here

Expected behavior As the Mock API returns the following Object, I would assume I can decode it to an array of users(I have two users with the same mail on my Mock API):

{ "data": { "UserByMail": { "items": [ { "firstName": "john", "lastName": "doe", "id": "3505288a-f06f-46bd-89ec-87990f1ed007", "gender": "male", "mail": "[email protected]", "participatingChats": { "items": [] } }, { "firstName": "john", "lastName": "doe", "id": "82e82ac8-a401-45b9-80cb-4c5e6d399cf1", "gender": "male", "mail": "[email protected]", "participatingChats": { "items": [] } } ]

I am really not sure how I can figure out the proper response type I should be decoding to.

C4RR13P0TT3R avatar Oct 24 '21 10:10 C4RR13P0TT3R

I now changed the response type of the function to a GraphQLRequest<JSONValue>. I am stillnot sure how to decode the response. When printing it out I get:

object(["items": Amplify.JSONValue.array([])])

C4RR13P0TT3R avatar Oct 26 '21 10:10 C4RR13P0TT3R

This feature is available by default now i believe.

raviscn avatar Jan 29 '22 18:01 raviscn