Amplify-cli: Mock GraphQL API using DataStore doesn't work

Created on 3 May 2020  路  12Comments  路  Source: aws-amplify/amplify-cli

Note: If your issue/bug is regarding the AWS Amplify Console service, please log it in the
Amplify Console GitHub Issue Tracker

Describe the bug
All operations (saving, syncing etc.) between my client (ReactJS app) and the mock API fail with various reasons. Whereas running the same exact API on cloud stack (after amplify push) works 100%.

My simple schema @model:

type Account
@model
{
    id: ID!
    givenName: String!
    familyName: String!
    email: String!
}

That generates the following GraphQL schema:

type Account {
  id: ID!
  givenName: String!
  familyName: String!
  email: String!
  _version: Int!
  _deleted: Boolean
  _lastChangedAt: AWSTimestamp!
}

enum ModelSortDirection {
  ASC
  DESC
}

type ModelAccountConnection {
  items: [Account]
  nextToken: String
  startedAt: AWSTimestamp
}

input ModelStringInput {
  ne: String
  eq: String
  le: String
  lt: String
  ge: String
  gt: String
  contains: String
  notContains: String
  between: [String]
  beginsWith: String
  attributeExists: Boolean
  attributeType: ModelAttributeTypes
  size: ModelSizeInput
}

input ModelIDInput {
  ne: ID
  eq: ID
  le: ID
  lt: ID
  ge: ID
  gt: ID
  contains: ID
  notContains: ID
  between: [ID]
  beginsWith: ID
  attributeExists: Boolean
  attributeType: ModelAttributeTypes
  size: ModelSizeInput
}

input ModelIntInput {
  ne: Int
  eq: Int
  le: Int
  lt: Int
  ge: Int
  gt: Int
  between: [Int]
  attributeExists: Boolean
  attributeType: ModelAttributeTypes
}

input ModelFloatInput {
  ne: Float
  eq: Float
  le: Float
  lt: Float
  ge: Float
  gt: Float
  between: [Float]
  attributeExists: Boolean
  attributeType: ModelAttributeTypes
}

input ModelBooleanInput {
  ne: Boolean
  eq: Boolean
  attributeExists: Boolean
  attributeType: ModelAttributeTypes
}

input ModelSizeInput {
  ne: Int
  eq: Int
  le: Int
  lt: Int
  ge: Int
  gt: Int
  between: [Int]
}

input ModelAccountFilterInput {
  id: ModelIDInput
  givenName: ModelStringInput
  familyName: ModelStringInput
  email: ModelStringInput
  and: [ModelAccountFilterInput]
  or: [ModelAccountFilterInput]
  not: ModelAccountFilterInput
}

enum ModelAttributeTypes {
  binary
  binarySet
  bool
  list
  map
  number
  numberSet
  string
  stringSet
  _null
}

type Query {
  syncAccounts(filter: ModelAccountFilterInput, limit: Int, nextToken: String, lastSync: AWSTimestamp): ModelAccountConnection
  getAccount(id: ID!): Account
  listAccounts(filter: ModelAccountFilterInput, limit: Int, nextToken: String): ModelAccountConnection
}

input CreateAccountInput {
  id: ID
  givenName: String!
  familyName: String!
  email: String!
  _version: Int
}

input UpdateAccountInput {
  id: ID!
  givenName: String
  familyName: String
  email: String
  _version: Int
}

input DeleteAccountInput {
  id: ID
  _version: Int
}

type Mutation {
  createAccount(input: CreateAccountInput!, condition: ModelAccountConditionInput): Account
  updateAccount(input: UpdateAccountInput!, condition: ModelAccountConditionInput): Account
  deleteAccount(input: DeleteAccountInput!, condition: ModelAccountConditionInput): Account
}

input ModelAccountConditionInput {
  givenName: ModelStringInput
  familyName: ModelStringInput
  email: ModelStringInput
  and: [ModelAccountConditionInput]
  or: [ModelAccountConditionInput]
  not: ModelAccountConditionInput
}

type Subscription {
  onCreateAccount: Account @aws_subscribe(mutations: ["createAccount"])
  onUpdateAccount: Account @aws_subscribe(mutations: ["updateAccount"])
  onDeleteAccount: Account @aws_subscribe(mutations: ["deleteAccount"])
}

Bootstrap DataStore hack (as first DataStore.save() operation was never syncing with cloud - suggested in another thread):

useEffect(() => {
        let subscription = null
        let cancel = false

        const init = async () => {
            subscription = DataStore.observe().subscribe(console.log('Hack for DataStore init.'))
        }

        if (!cancel) {
            init()
        }

        return () => {
            cancel = true

            if (subscription) {
                subscription.unsubscribe()
            }
        }
    }, [])

An instance is created through a click handler, and a subscription is created at that point too:

const account = await DataStore.save(
                new Account({
                    email: "help.me@thanks",
                    familyName: "Doe",
                    givenName: "John",
                })
            )

const accountSubscription = DataStore.observe(Account, account.id).subscribe(msg => {
                console.log(msg.model, msg.opType, msg.element)
            })

The DataStore.save() operation correctly creates and saves the Account locally (in IndexedDb), but returns the following error when trying to sync with Mock API:

{data: {createAccount: null},鈥
data: {createAccount: null}
createAccount: null
errors: [{message: "Cannot return null for non-nullable field Account._version.",鈥]
0: {message: "Cannot return null for non-nullable field Account._version.",鈥
locations: [{line: 7, column: 5}]
message: "Cannot return null for non-nullable field Account._version."
path: ["createAccount", "_version"]
0: "createAccount"
1: "_version"

And further to this, it tries to begin syncing with GraphQL requests, which returns:

{data: {syncAccounts: null},鈥
data: {syncAccounts: null}
syncAccounts: null
errors: [{message: "Unknown operation name: Sync", errorType: null, data: null, errorInfo: null,鈥]
0: {message: "Unknown operation name: Sync", errorType: null, data: null, errorInfo: null,鈥
data: null
errorInfo: null
errorType: null
locations: [{line: 2, column: 3, sourceName: "GraphQL request"}]
0: {line: 2, column: 3, sourceName: "GraphQL request"}
column: 3
line: 2
sourceName: "GraphQL request"
message: "Unknown operation name: Sync"
path: ["syncAccounts"]
0: "syncAccounts"

Amplify CLI Version
4.18.1

To Reproduce

  • Create an API through amplify api add
  • Create the Account object in amplify/backend/api/app/schema.graphql
  • Run amplify codegen models
  • Run amplify api update
    -- Api key authentication
    -- Conflict detection = Optimistic concurrency
    -- Enable DataStore for entire API
  • Run amplify mock
  • Open ReactJS app and observe console/request errors

Expected behavior
The Creating and syncing should produce no errors, as is the case when amplify push to cloud and running the app against a "live" API. Mock environment should work in the same way, and is needed to speed up dev!

Desktop (please complete the following information):
Windows 10 Pro build 19041 running WSL2 Ubuntu
This stack is setup on Ubuntu - 18.04

NodeJS version: v13.13.0

DataStore feature-request pending-review

Most helpful comment

@undefobj Is there any update related to this feature?

All 12 comments

Further investigation revealed that adding @auth directive to my model allowed the objects to be created in the Mock API GraphQL database successfully, however the create operation still returns the error.

message: "Cannot return null for non-nullable field Account._version.", operation: "Create"

My updated model declaration:

type Account
@model
@auth(rules: [
    {allow: owner}
])
{
    id: ID!
    givenName: String!
    familyName: String!
    email: String!
}

It seems like the version resolver is not working in a mock environment, and therefore breaking the synchronization flow?

@blydewright Amplify Mock does not have support for sync resolvers. To test DataStore syncing, the API will have to be deployed to AppSync.

Is there any specific reason why you would like to run the DataStore using mock? DataStore should work locally without any servers when sync is not enabled.

@blydewright Amplify Mock does not have support for sync resolvers. To test DataStore syncing, the API will have to be deployed to AppSync.

Is there any specific reason why you would like to run the DataStore using mock? DataStore should work locally without any servers when sync is not enabled.

For me, it is a much better development experience to use Amplify mock. It enables quicker iteration during development. Especially, if you are making many changes to the schema. Amplify push takes long enough that it can be slightly annoying when making many changes or trying out new things.

Another thing I noticed was that Amplify mock forces you to supply all the values in a model. Otherwise it will throw an error like the following:

Error: Field email should be of type string, undefined received

With an AppSync deployment it do not get this error when not providing optional fields in a model.

@yuth as @arnm pointed out above, the development experience and speed in using Mock is the primary advantage. I myself wanted to test flows of using sync locally before pushing to cloud, as I needed to update my schema many many times, and manually deleting the mock sqlite database when I wanted to re-initialise locally, was far easier then destroying an entire environment and re-deploying it again from scratch as I had to do multiple times whilst learning about limitations in modifying keys/relationships between models during deploys.

As AppSync doesn't support mock environment right now, perhaps it can be discussed as a potential improvement? Is the lack of AppSync/mock environment support documented anywhere? I found the lack of substantial (up-to-date) documentation to be my biggest hurdle in learning and making progress... otherwise I'm quite excited to be using Amplify :)

This issue has been automatically closed because of inactivity. Please open a new issue if you are still encountering problems.

@blydewright @arnm We can make this a feature request for mock, however the key is that the Conflict Resolution will not be locally mockable. For instance Auto Merge logic. Is something very simplistic with mock (accept all writes even on conflict, or alternatively reject on conflict) ok for you?

@undefobj Yes, that is understandable. If we can define a simple strategy for local mocking, wether it be cached/offline values overwrite any conflicts in mock db or the other way around, works fine with me.

Please reference that new issue here for visibility! Thanks!

@undefobj sounds good :) Thanks for considering this as a feature request 馃憤

@blydewright @arnm We can make this a feature request for mock, however the key is that the Conflict Resolution will not be locally mockable. For instance Auto Merge logic. Is something very simplistic with mock (accept all writes even on conflict, or alternatively reject on conflict) ok for you?

I was wondering would if it be possible for local mock env to create a local table and also an additional table that pretended to be the cloudDB (but also running on local machine), as this might let us keep Conflict Resolution. @undefobj would that be better than disabling Conflict Resolution?

@undefobj Is there any update related to this feature?

@undefobj Is there any update related to this feature?

seconded, bump

I'm having this same error, only with an API error message that's returned from the mock API with a message of _"Cannot return null for non-nullable field... _version"_. It sounds like this may be the same issue as described above. Note that the API is the only thing I'm running in local mock mode. All else (e.g. Auth, Functions, ....) are all running in AWS cloud.

Note also that I do NOT get this error if when doing amplify update api or amplify add api I choose _No_ when asked "Configure conflict detection?" Choosing any other option will produce this error in local mock mode. So, not having conflict detection (in development mode) does provide a workaround... UNLESS you need to use any update queries... which will NOT be generated by amplify codegen unless conflict detection is configured!! This appears to be an egregious oversite!!!

Despite this (sort of) workaround for the _version (which I have no apparent control over), using @connect causes similar errors to be thrown for any nested data as in the following code:

type MyUser
  @model
  @auth(
    rules: [
      { allow: public, provider: apiKey }, 
      { allow: private, provider: iam }, 
      { allow: owner },
    ]
  )
  @key(fields: ["id"])
{
  id: ID!
  email: AWSEmail!
  username: String
  firstName: String
  lastName: String
  profilePicThumbnail: Image @connection
}

type Image
  @model 
  @auth(
    rules: [
      { allow: public, provider: apiKey }, 
      { allow: private, provider: iam }, 
      { allow: owner },
    ]
  )
  @key(fields: ["id"])
{
  id: ID!
  title: String
  description: String
  isThumbnail: Boolean!
}

profilePicThumbnail throws _"Cannot return null for non-nullable field"_ errors when no data is provided in the database, even though it's not a required field! This is VERY problematic! Is there a workaround for this?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

amlcodes picture amlcodes  路  3Comments

davo301 picture davo301  路  3Comments

gabriel-wilkes picture gabriel-wilkes  路  3Comments

mwarger picture mwarger  路  3Comments

MageMasher picture MageMasher  路  3Comments