amplify-category-api icon indicating copy to clipboard operation
amplify-category-api copied to clipboard

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

Open neaumusic opened this issue 1 year ago • 3 comments

Before opening, please confirm:

  • [X] I have installed the latest version of the Amplify CLI (see above), and confirmed that the issue still persists.
  • [X] I have searched for duplicate or closed issues.
  • [X] I have read the guide for submitting bug reports.
  • [X] I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
  • [X] I have removed any sensitive information from my code snippets and submission.

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

# 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

neaumusic avatar Oct 18 '22 18:10 neaumusic