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

Cannot have 2 different hasOne relations to the same type with belongsTo

Open clintfoster opened this issue 3 years 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.

How did you install the Amplify CLI?

npm

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

v16.13.1

Amplify CLI Version

7.6.13

What operating system are you using?

Mac

Amplify Codegen Command

codegen models

Describe the bug

Two different fields with a hasOne relation to the same type causes the following error if it has a reverse-link (belongsTo) field:

A 'belongsTo' field should match to a corresponding 'hasMany' or 'hasOne' field

For example, consider a user that can have home & shipping addresses (may or may not be the same):

User.homeAddr: Addr @hasOne
User.shipAddr: Addr @hasOne

Addr.user: User @belongsTo

The same error occurs if a separate field is used for each reverse link:

Addr.homeUser: User @belongsTo
Addr.shipUser: User @belongsTo

The error does not occur if the belongsTo annotation is removed.

It's only a problem for amplify codegen models, not amplify api gql-compile or the generated Dynamo tables.

Expected behavior

It should be possible to model something like a user with two addresses without resorting to a one-to-many relationship. We are not modeling a list of addresses, only two specific addresses. With a list of addresses, we would have to give each a type. Then we would have to verify only one of each type occurs in the list. Also, to access an address of a specific type we would have to search the list.

Reproduction steps

In a fresh directory do the following, accepting the defaults:

amplify init
amplify add api

Replace schema.graphql with the one in the next section, then continue as follows:

amplify api gql-compile
amplify codegen models

Notice gcl-compile succeeds, but codegen returns the error mentioned in the description.

GraphQL schema(s)

type User @model {
  id: ID! @primaryKey
  homeAddr: Addr @hasOne
  shipAddr: Addr @hasOne
}

type Addr @model {    
  id: ID! @primaryKey
  user: User @belongsTo
  
# workaround (doesn't work)  
#   homeUser: User @belongsTo
#   shipUser: User @belongsTo
}

Log output

# Put your logs below this line


Additional information

Stack trace from codegen models

A 'belongsTo' field should match to a corresponding 'hasMany' or 'hasOne' field
Error: A 'belongsTo' field should match to a corresponding 'hasMany' or 'hasOne' field
    at Object.processBelongsToConnection (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/utils/process-belongs-to.ts:40:11)
    at Object.processConnectionsV2 (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/utils/process-connections-v2.ts:136:16)
    at /Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts:691:32
    at Array.forEach (<anonymous>)
    at /Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts:690:20
    at Array.forEach (<anonymous>)
    at AppSyncModelJavascriptVisitor.processConnectionDirectivesV2 (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts:689:34)
    at AppSyncModelJavascriptVisitor.processDirectives (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts:303:12)
    at AppSyncModelJavascriptVisitor.generate (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/visitors/appsync-javascript-visitor.ts:50:10)
    at Object.exports.plugin (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@aws-amplify/appsync-modelgen-plugin/src/plugin.ts:54:20)
    at Object.executePlugin (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@graphql-codegen/core/src/execute-plugin.ts:54:12)
    at /Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@graphql-codegen/core/src/codegen.ts:74:28
    at Array.map (<anonymous>)
    at Object.codegen (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/@graphql-codegen/core/src/codegen.ts:61:21)
    at /Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/amplify-codegen/src/commands/models.js:131:23
    at Array.map (<anonymous>)
    at Object.generateModels (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/amplify-codegen/src/commands/models.js:130:46)
    at Object.run (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/amplify-codegen/commands/codegen/models.js:9:7)
    at Object.executeAmplifyCommand (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/node_modules/amplify-codegen/src/amplify-plugin-index.js:9:3)
    at executePluginModuleCommand (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/src/execution-manager.ts:178:3)
    at executeCommand (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/src/execution-manager.ts:30:5)
    at Object.run (/Users/clint/.nvm/versions/node/v16.13.1/lib/node_modules/@aws-amplify/cli/src/index.ts:205:5)

clintfoster avatar Jan 28 '22 22:01 clintfoster

Hi @clintfoster. It looks like such access pattern might not be supported by Datastore. One alternative is to model Home and Ship Addresses separately like below:

type User @model {
  id: ID! @primaryKey
  homeAddr: HomeAddr @hasOne
  shipAddr: ShipAddr @hasOne
}

type HomeAddr @model {    
  id: ID! @primaryKey
  user: User @belongsTo
}

type ShipAddr @model {    
  id: ID! @primaryKey
  user: User @belongsTo
}

phani-srikar avatar Jan 31 '22 18:01 phani-srikar

Thanks for the response, @phani-srikar. The workaround you described seems reasonable, but it presents a few problems. We actually have three types of addresses:

  • home
  • billing
  • shipping

When all three are the same (usually the case), they should point to the same Addr. Otherwise the user must make changes in three places.

Also, we have indexes allowing administrators to search by address and retrieve the User via belongsTo. If there are three tables they will need to search all three (or know which addresses were specified).

I guess it goes without saying that a model that doesn't match the problem introduces complications. The real-world situation is a User that may have one, two or three addresses (which may be different or the same). Just as a list is a poor model for this problem, so are three separate address types with duplicate fields.

Notice the generated GraphQL input types are straightforward. It is easy to understand how the API works.

export type CreateAddrInput = {
  addrUserId?: string | null
};

export type CreateUserInput = {
  userHomeAddrId?: string | null,
  userShipAddrId?: string | null,
  userMailAddrId?: string | null
};

The representation in Dynamo is also straightforward.

Is it possible to request this as a feature?

clintfoster avatar Jan 31 '22 19:01 clintfoster

We are currently investigating the feasibility of the change.

dpilch avatar Feb 02 '22 16:02 dpilch