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

Datastore observeQuery not recieving remote updates on some models after initial sync

Open Jesmaster opened this issue 2 years ago • 1 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?

No response

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

No response

Amplify CLI Version

9.2.1

What operating system are you using?

Ubunutu Mate

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

No manual changes made.

Amplify Categories

api

Amplify Commands

Not applicable

Describe the bug

I use observeQuery for several models in my application so I can display any changes immediately to the user without them having to reload. This is working fine for changes that are happening locally for the current user using the app, for example they make a change to a record via a form on the app and the observeQuery subscription immediately sees the changes.

However when a remote change happens then the observeQuery subscription does not pick up the change immediately and the user would need to hard refresh their browser or log out and back in to pick up the change. Examples of this kind of change could be a record being updated via a lambda function, or going into Amplify Studio and making a change, or another user using the app changes a public record then that user would see the change right away but others would need to hard refresh.

The strange thing here is one of my models DOES process remote changes as expected but as far as I can tell none of my other ones do. I've posted my schema.graphql below, the model that does work is Profile, an example of one that doesn't work is Score

Expected behavior

observeQuery should always detect remote changes to the subscribed model

Reproduction steps

This Works Correctly

  1. create an observeQuery subscription to the Profile model and log to the console any time there is a change
        const subscription = DataStore.observeQuery(Profile).subscribe(snapshot => {
            const { isSynced, items } = snapshot;

            isSynced && console.log(items);
        });
  1. Go into Amplify Studio and create a new Profile record or modify an existing one, you will see the log coming up in the console correctly.

This Does Not Work (Same code, different model)

  1. create an observeQuery subscription to the Score model and log to the console any time there is a change
        const subscription = DataStore.observeQuery(Score).subscribe(snapshot => {
            const { isSynced, items } = snapshot;

            isSynced && console.log(items);
        });
  1. Go into Amplify Studio and create a new Score record or modify an existing one, you will see not see any logs for any remote changes. You will only see logs for local changes.

GraphQL schema(s)

# Put schemas below this line
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules

#input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!

# Naming rules follows:
# https://graphql-rules.com/rules/naming

# AppSync Scalar Type def:
# https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html

# Misc
# Subscription type not available - https://github.com/aws-amplify/amplify-cli/issues/5554

# ** For visible, add all users to group members, and add member to groups attribute when we want to make visible


type Score @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read]}
  ]) {
  id: ID!  
  groups: [String]
  name: String!
  profile: Profile! @belongsTo
  derivesFromId: ID @index(name: "byDerivesFromId", queryField: "scoreByDerivesFromId")
  scoreData: AWSJSON
  publicScore: PublicScore @hasOne
  groupScore: GroupScore @hasOne  
  editable: Boolean!
  visible: Boolean!
  shareable: Boolean!
  community: Boolean
  completed: Boolean
  groupVisible: Boolean @default(value: "false")
  #profileId: ID @index(name: "byProfile", queryField: "scoreByProfileId")
}


type PublicScore @model @auth(rules:[
    {allow: public, provider: iam, operations: [read]},
    {allow: owner,  operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read]}
  ]) {
  id: ID!
  groups: [String]
  name: String!
  derivesFromId: ID @index(name: "byDerivesFromId", queryField: "publicScoreByDerivesFromId")
  score: Score @belongsTo
  scoreId: ID! @index(name: "publicScoreByScoreId", queryField: "listPublicScoresByScoreId")
  scoreData: AWSJSON
  editable: Boolean! 
  visible: Boolean!
}

type CommunityScore @model @searchable @auth(rules:[
    {allow: private, provider: iam, operations: [read]},
    {allow: owner,  operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!
  groups: [String]
  name: String!
  scoreId: ID!
  profileId: ID!
  profileName: String!
  scoreData: AWSJSON
  editable: Boolean!
  visible: Boolean!
  featured: Boolean!
  sortProfileName: String
  sortName: String
  sortTimestamp: AWSTimestamp
}

type GroupScore @model @searchable @auth(rules:[
    {allow: owner,  operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
    {allow: groups, groupsField: "groups", operations: [read]},
    {allow: private, provider: iam, operations: [read, update]}
  ]) {
  id: ID!
  groups: [String]
  name: String!
  derivesFromId: ID @index(name: "byDerivesFromId", queryField: "groupScoreByDerivesFromId")
  score: Score @belongsTo
  scoreId: ID!
  profileId: ID
  profileName: String
  scoreData: AWSJSON
  editable: Boolean!
  visible: Boolean!
  featured: Boolean!
  completed: Boolean @default(value: "false")
  groupVisible: Boolean @default(value: "false")
  class: Class @belongsTo
  archived: Boolean @default(value: "false")
}

type Profile @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: private, operations: [read, update], provider: iam},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!  
  groups: [String] # e.g. class101_admin (teacher group)
  name: String! @index(name: "profileByName", queryField: "listProfilesByName")
  capabilities: AWSJSON
  isDefault: Boolean
  archived: Boolean @default(value: "false")
  class: Class @belongsTo
  account: Account @belongsTo
  user: User @belongsTo
  scores: [Score] @hasMany
  owner: ID
}

type Class @model @searchable @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: private, operations: [read, update], provider: iam},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read]}
  ]) {
  id: ID!  
  groups: [String] # e.g. class101_admin (teacher group)
  name: String!
  account: Account @belongsTo
  profiles: [Profile] @hasMany
  groupScores: [GroupScore] @hasMany
  studentInvitations: [StudentInvitation] @hasMany
  archived: Boolean
  organization: Organization @belongsTo
  owner: ID
}

type EducatorInvitation @model @searchable @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: public, provider: iam, operations: [read]},
    {allow: private, operations: [read, update], provider: iam},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read]}
]) {
  id: ID!
  groups: [String]
  firstName: String!
  lastName: String!
  email: String!
  accepted: Boolean!
  organization: Organization! @belongsTo
  educatorUserId: String
  lastResend: AWSTimestamp
}

type StudentInvitation @model @searchable @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: public, provider: iam, operations: [read]},
    {allow: private, operations: [read, update, delete], provider: iam},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read]}
]) {
  id: ID!
  groups: [String]
  firstName: String!
  lastName: String!
  email: String!
  class: Class! @belongsTo
  accepted: Boolean!
  studentUserId: String
  lastResend: AWSTimestamp
  owner: ID
}

type User @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!
  email: String # email address
  groups: [String] # e.g. class101_admin (teacher group)
  name: String
  profiles: [Profile] @hasMany
  account: Account! @belongsTo
  #userprofiles: [Profile] @hasMany(indexName: "byUser", fields: ["id"])  
}

enum AccountType {
  INDIVIDUAL,
  STUDENT,
  EDUCATOR,
  EDUCATOR_ADMIN,
  EVENT,
  SUPPORT,
  ADMIN
}

type Account @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: private, operations: [read, update], provider: iam},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read, create, update, delete]}
  ]) {
  id: ID!  
  groups: [String] # e.g. class101_admin (teacher group)
  accountType: AccountType!
  master: Account @hasOne
  user: User @hasOne
  profiles: [Profile] @hasMany
  classes: [Class] @hasMany
  plan: [Plan] @hasMany
  owner: ID
}

type Organization @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
    {allow: groups, groupsField: "groups", operations: [read]},
    {allow: public, operations: [read], provider: iam},
    {allow: private, operations: [read, update], provider: iam}
  ]) {
  id: ID!
  groups: [String]
  capabilities: AWSJSON!
  email: String!
  name: String!
  seatLimit: Int!
  educatorInvitations: [EducatorInvitation] @hasMany
  classes: [Class] @hasMany
  accepted: Boolean @default(value: "false")
}

type Transaction @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!
  groups: [String]
  plan: Plan @belongsTo
  stripeSessionId: String
  stripeCustomerId: String
  stripeSubscriptionId: String @index(name: "transactionByStripeSubscriptionId", queryField: "listTransactionsByStripeSubscriptionId")
}
  
type Plan @model @auth(rules:[
    {allow: owner, operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!  
  groups: [String]
  transaction: Transaction @hasOne
  account: Account! @belongsTo
  offering: Offering! @belongsTo
  startDate: AWSDate!
  endDate: AWSTimestamp
  active: Boolean!
  owner: ID
}


# plans[] should be safe since plan doesn't allow private/public access
type Offering @model @auth(rules:[
    {allow: public, provider: iam, operations: [read]},
    {allow: private, operations: [read]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!  
  groups: [String]
  name: String!
  description: String
  plans: [Plan] @hasMany
  durationDays: Int
  price: String
  isDefault: Boolean
  enabled: Boolean!
  visible: Boolean!
  contentUrl: AWSURL
  iframeContentUrl: AWSURL
  capabilities: AWSJSON
  retiredDate: AWSDate
  scoreLimit: Int
  seatLimit: Int
  schoolLimit: Int
  stripePriceId: String
}

type Content @model @auth(rules:[
    {allow: public, provider: iam, operations: [read]},
    {allow: private, operations: [read]},
    {allow: owner, operations: [read, create, update, delete]},
    {allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
  ]) {
  id: ID!  
  groups: [String]
  key: String!
  value: String  
  description: String
}

Log output

No response

Additional information

No response

Jesmaster avatar Aug 11 '22 18:08 Jesmaster

Hi @Jesmaster 👋 I would test any of your models that are using a dynamic group authorization rule and those that are only using static groups. There is a known limitation for subscriptions with dynamic group authorization.

For more info: https://docs.amplify.aws/cli/graphql/authorization-rules/#user-group-based-data-access

It might also be worth checking what auth mode DataStore is subscribing to the model with? You may be able to find it if you check the Network Activity tab for something like this:

Screenshot 2022-09-02 at 3 12 55 PM

Then, in the Payload tab there should be a Header that suggests what kind of authMode was used to subscribe, in my case it was an API_KEY.

Screenshot 2022-09-02 at 3 15 05 PM

Lastly, are there any errors in the console that suggest the user was not authorized to subscribe or receive any data when creating/updating a record remotely?

chrisbonifacio avatar Sep 02 '22 19:09 chrisbonifacio

Hi 👋 Closing this as we have not heard back from you. If you are still experiencing this issue and in need of assistance, please feel free to comment and provide us with any information previously requested by our team members so we can re-open this issue and be better able to assist you.

Thank you!

tannerabread avatar Oct 07 '22 16:10 tannerabread