aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
88 stars 76 forks source link

Amplify Mock LSI fetches base table properties but AppSync Queries don't #897

Open neaumusic opened 1 year ago

neaumusic commented 1 year ago

Before opening, please confirm:

How did you install the Amplify CLI?

yarn

If applicable, what version of Node.js are you using?

14.20.1

Amplify CLI Version

10.2.3

What operating system are you using?

Mac

Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.

No, all new stack and CLI only

Amplify Categories

api

Amplify Commands

Not applicable

Describe the bug

Specifying a @hasMany relationship targeting an LSI index with KEYS_ONLY allows queries in amplify mock that fetch from the related table base (not just keys in the LSI index). Running the same queries via AWS Console AppSync does not, returns null for additional fields, throwing errors if those fields are declared as non-nullable.

From the dynamodb docs:

When you query a local secondary index, the query can also retrieve attributes that are not projected into the index. DynamoDB automatically fetches these attributes from the base table, but at a greater latency and with higher provisioned throughput costs.

See the bottom row in this table

If you query or scan a local secondary index, you can request attributes that are not projected in to the index. DynamoDB automatically fetches those attributes from the table.

Expected behavior

I would expect the AWS Console Appsync Queries to fetch non-projected attributes from the base table, at a slightly higher latency and read cost.

Reproduction steps

  1. amplify init
  2. amplify add auth (Cognito)
  3. add user group Admin
  4. amplify add api blank schema
  5. paste schema into schema.graphql
  6. edit cli.json to set "secondarykeyasgsi": false,
  7. amplify override api, paste following KEYS_ONLY code into override.ts
import { AmplifyApiGraphQlResourceStackTemplate } from '@aws-amplify/cli-extensibility-helper';

export function override(resources: AmplifyApiGraphQlResourceStackTemplate) {
  Object.values(resources.models).forEach(modelDirectiveStack => {
    modelDirectiveStack.modelDDBTable?.addPropertyOverride("LocalSecondaryIndexes.0.Projection.ProjectionType", "KEYS_ONLY");
    modelDirectiveStack.modelDDBTable?.addPropertyOverride("LocalSecondaryIndexes.1.Projection.ProjectionType", "KEYS_ONLY");
    modelDirectiveStack.modelDDBTable?.addPropertyOverride("LocalSecondaryIndexes.2.Projection.ProjectionType", "KEYS_ONLY");
    modelDirectiveStack.modelDDBTable?.addPropertyOverride("LocalSecondaryIndexes.3.Projection.ProjectionType", "KEYS_ONLY");
    modelDirectiveStack.modelDDBTable?.addPropertyOverride("LocalSecondaryIndexes.4.Projection.ProjectionType", "KEYS_ONLY");
  });
}
  1. amplify mock
  2. in mock environment, run mutation createContent with SK: "1234", run mutation createImage with SK: "asdf" and LSI2: "1234"
  3. in mock environment, run query listContent, specify all items of the content, all items of the images
  4. in AppSync Console, repeat the mutation (PK: "_CONTENT" for both) and query steps
  5. notice the error fetching non-index properties (like updatedAt) that are non-nullable

GraphQL schema(s)

Note Content and Image share PK: _CONTENT, and that Content @hasMany targets Image LSI2

```graphql # Put schemas below this line enum _CONTENT { _CONTENT } type Content @model(timestamps: { createdAt: "LSI1", updatedAt: "updatedAt" }) @auth(rules: [{ allow: groups, groups: ["Admin"] }]) { PK: _CONTENT! @primaryKey(sortKeyFields: ["SK"]) @index(name: "LSI1", sortKeyFields: ["LSI1"], queryField: "contentByLSI1") @index(name: "LSI2", sortKeyFields: ["LSI2"], queryField: "contentByLSI2") @index(name: "LSI3", sortKeyFields: ["LSI3"], queryField: "contentByLSI3") @index(name: "LSI4", sortKeyFields: ["LSI4"], queryField: "contentByLSI4") @index(name: "LSI5", sortKeyFields: ["LSI5"], queryField: "contentByLSI5") SK: String! # ID LSI1: String # createdAt LSI2: String LSI3: String LSI4: String LSI5: String # Image *LSI2* match Content *PK/SK* fields images: [Image]! @hasMany(indexName: "LSI2", fields: ["PK", "SK"]) updatedAt: AWSDateTime! } type Image @model(timestamps: { createdAt: "LSI1", updatedAt: "updatedAt" }) @auth(rules: [{ allow: groups, groups: ["Admin"] }]) { PK: _CONTENT! @primaryKey(sortKeyFields: ["SK"]) @index(name: "LSI1", sortKeyFields: ["LSI1"], queryField: "imageByLSI1") @index(name: "LSI2", sortKeyFields: ["LSI2"], queryField: "imageByLSI2") @index(name: "LSI3", sortKeyFields: ["LSI3"], queryField: "imageByLSI3") @index(name: "LSI4", sortKeyFields: ["LSI4"], queryField: "imageByLSI4") @index(name: "LSI5", sortKeyFields: ["LSI5"], queryField: "imageByLSI5") SK: String! # ID LSI1: String # createdAt LSI2: String # parent ID LSI3: String LSI4: String LSI5: String updatedAt: AWSDateTime! } ```

Project Identifier

529fd62f57a7db0816abf0c159f3a791

Log output

No response

Additional information

No response

ykethan commented 1 year ago

Hey @neaumusic, thank you for reaching out. On creating a GraphQL API with the provided reproduction steps. I was unable to observe an error when adding properties such as updatedAt.

image

could you let know if there are any additional step utilized in the reproduction?

neaumusic commented 1 year ago

@ykethan in that screenshot, there should be images correlated with the content, the SK and LSI2 are backwards on your image model

the image should say "PK": "_CONTENT", "SK: "asdf", "LSI2": "1234"

then just run listContents to fetch items/* and items/images/items/*

the error will occur in fetching items/images/items/* since the rest of the image attributes are not projected onto the LSI and the query doesn't fetch from the base dynamodb table

neaumusic commented 1 year ago

@ykethan I don't know if you ever got back around to this one, but it should still be an issue