Open hisham opened 6 years ago
GraphQL with interfaces compiled with no issues so closing this for now.
Actually, based on the example at https://docs.aws.amazon.com/appsync/latest/devguide/interfaces-and-unions.html, how would you use graphql transformer to model this?
How I'd think of it is that there would be one "Event" table and so that would be the actual "@model" and different types of objects in it as follows. Since you're using "__typename" in the db table to distinguish which type of object and item is, then this is akin to single table inheritance in the RDBMS world. Correct? But doesn't look like amplify cli supports this since I get this error when I run the below schema:
Schema Errors:
Directive "model" may not be used on INTERFACE.
GraphQL request (13:16)
12:
13: interface Event @model {
^
interface Event @model {
id: ID!
name : String!
startsAt: String
endsAt: String
venue: Venue
minAgeRestriction: Int
}
type Concert implements Event {
id: ID!
name: String!
startsAt: String
endsAt: String
venue: Venue
minAgeRestriction: Int
performingBand: String
}
type Festival implements Event {
id: ID!
name: String!
startsAt: String
endsAt: String
venue: Venue
minAgeRestriction: Int
performers: [String]
}
type Conference implements Event {
id: ID!
name: String!
startsAt: String
endsAt: String
venue: Venue
minAgeRestriction: Int
speakers: [String]
workshops: [String]
}
It doesn't make sense to mark Conference, Festival, and Concert here as "@model" because then 3 tables would be created and the whole point of "interface" would be lost. But please correct me if I am wrong...
Hey these are all interesting points. The short answer is that the transform allows you to define interfaces and use them however you want but it does not support @model directives on interface types. This could 100% be a feature request that when using @model on an interface type, then we will store all object types that implement that interface in the same table using the __typename as the hash key and some uuid as the sort key but this has implications (esp. around how @connection would work) that should be discussed.
"the whole point of "interface" would be lost."
I don't agree with this as the point of interfaces is to express that multiple object types share a common set of attributes at the API level. This makes no implication about where or how your data is stored and thus it is really irrelevant if its 3 tables or 1. In your particular use case you want 1 table, but someone else might want 3 and both are totally reasonable uses of interface types.
Hi @mikeparisstuff thanks for the detailed response.
Fair point that in some cases people might want 3 tables instead of 1. And now with your support of custom queries and planned support for custom resolvers I think one would be able to write a query to get all Events even if they are stored in 3 different tables.
My particular use case is more of where the interface is a "User" type and the particular implementations can be something like "Doctor", "Patient", and "Staff" where these users can all do things like login but have very different views and permissions in the app.
I think it would make sense to have all Users, regardless of type, be in one table. You can argue I can use Cognito groups to differentiate between users but I think my functionality here goes beyond permissions and more of also the type of data stored for each user type and the relationships they have with other models in the system. Also DynamoDB's NoSql guide recommends to minimize the number of tables so this seems like a good case where only one table is needed rather than 3.
I am interested in what implications you are hinting at around how @connection would work if I go with the above plan. Any pitfalls I should be aware of or best practice recommendations would be appreciated. Currently I am only using the graphql transformer to see how amplify does things and my real appsync setup is done by hand since we need custom resolvers and ability to connect to a dynamo database and lambda methods in a different region than where appsync is hosted.
This makes sense. Unfortunately, I'm not sure this is going to be supported in the transform out of the box yet but you could write your own transformer that does just this. You can read more about how to do that here: https://github.com/aws-amplify/amplify-cli/blob/master/how-to-write-a-transformer.md
@mikeparisstuff Adding a type where @model on an interface would be preferred, using the model from @hisham
type Ticket @model { id: ID! event: Event @connection }
I believe that this is related so I'm asking it here but I can open another question if that would be more appropriate.
I am trying to create a connection between two tables. I would like the child table to contain different types of objects that implement a similar interface. This issue indicates that I can't keep all of the child types in the same table. I tried to keep them in separate tables but then I don't think that @connection
will support that.
I tried something similar to the following:
type Container @model {
...
things: [Thing] @connection(name: "Things")
}
interface Thing {
...
container: Container!
}
type Thing1 implements Thing @model {
...
container: Container! @connection(name: "Things")
}
type Thing2 implements Thing @model {
...
container: Container! @connection(name: "Things")
}
I get "InvalidDirectiveError: Could not find an object type named Thing". I think that the right way to solve this is to allow multiple types in one table rather than allowing @connection
to reference multiple tables. Thoughts?
I'm needing to do the same thing as Erik. Basically, a many-to-many relationship between a type
and an interface
.
Related: aws-amplify/amplify-cli#91
Also just ran into this error –– sounds like the @model
transformer on interfaces could be a great addition to the CLI
Really need interface to support @model and the implementing types to use a single table. For example if we have a type of Post and a TextPost or ImagePost I would like all of the Posts to be contained in a single table.
Also interested in interface support. My scenario involves a 'FieldType' interface in which they all share the same DynamoDB table. I've implemented this through my own resolvers, but when CLI v3 came out I found myself going back and updating all of the custom resolver logic to mirror the new @auth directive enhancements.
+1 for feature My scenario involves a single "Person" type but variations (e.g. Singer, Dancer etc.) where each may have a specific relation to another object. BUT I would like all "persons" to be stored in a single table. The rationale is that a person - say Jane - can be a singer as well as dancer I just need one "person" object stored with different tags/aspects.
@hisham would you, please, share with us how you implemented the interface @model
behaviour?
I wrote a node script that modifies the cloudformation json files amplify cli generates (e.g. ConnectionStack.json and the model stack jsons). This script is run before any amplify push. Let's say if you have interface that 3 models implement. Amplify CLI will want to create 3 tables and data sources for these. So my script creates 1 table as the data source and connects the 3 models to that data source.
It's a temporary workaround, but has been running fine and survived various amplify cli updates. Hopefully an official solution comes out at some point.
@hisham could you, maybe, share your script on https://gist.github.com/? I would really appreciate it. I have the same use case and thought about using the ENUM
to differentiate between different user types.
Btw. do you use amplify mock
and if you do, does the script also creates 1 table locally?
Thank you very much in advance!!!
Sure here you go: https://gist.github.com/hisham/93ac6fcbe66f4a346d32681dc83e5ce2/revisions
The part that is not in the gist is you'll have to create a User.json and UserTable.json nested cloudformation stack (for the table you want the models to actually be stored in + appsync connections). These you will have to create manually through a custom nested stack, which amplify cli supports. They are similar tables and setup to what amplify cli creates for @model decorated types so basically copy the json amplify cli creates for one of your models that implement the interface and change some names in it to what your interface is called.
Now that amplify cli supports custom transformers, there's probably a better solution out there via graphql transformers. Would be nice if someone builds that at some point! :)
@hisham Thank you very much 👍
No problem. To answer your other question, I haven't tried amplify mock yet for appsync. I did try it once or twice but ran into an issue and never looked into it deeply. It's on my todo list. I don't think it's related to my script.
Sure here you go: https://gist.github.com/hisham/93ac6fcbe66f4a346d32681dc83e5ce2/revisions
The part that is not in the gist is you'll have to create a User.json and UserTable.json nested cloudformation stack (for the table you want the models to actually be stored in + appsync connections). These you will have to create manually through a custom nested stack, which amplify cli supports. They are similar tables and setup to what amplify cli creates for @model decorated types so basically copy the json amplify cli creates for one of your models that implement the interface and change some names in it to what your interface is called.
Now that amplify cli supports custom transformers, there's probably a better solution out there via graphql transformers. Would be nice if someone builds that at some point! :)
Thank you very much @hisham , this is exactly what we are looking for but we are struggling to reproduce this technique. We have not been successful in correctly recreating the "ConnectionStack.json" file referenced in your "modifyAmplifyCF.js" as we were expecting to see @connection used somewhere in the example in the header:
interface User {
id: ID!
email: AWSEmail!
}
type Doctor implements User @model {
id: ID!
email: AWSEmail
licenseNumber: Int
}
type Patient implements User @model {
id: ID!
email: AWSEmail
healthcardNumber: Int
}
Additionally, we are still trying to figure out what should be in the different stacks ("User.json", "UserTable.json", "Doctor.json", and "Patient.json") as we hypothesized that "UserTable.json" would be equivalent to an amplify created @model template, but that significant changes would be necessary to produce a "User.json" interface as well as "Doctor.json" and "Patient.json" interface implementing types belonging to the "UserTable".
If you have time and if you are comfortable with sharing this information, we would greatly appreciate it if you could share:
We greatly appreciate any help, and we are very grateful that you have found a workaround for this issue as many sleepless nights have led us here.
To provide a little more context, our overall objective is to produce a single DynamoDB table for our application with a similar structure using an interface (or ideally a Union but we have been unable to find working examples utilizing amplify), and then build batch mutation, query, and getitem pipeline resolvers to satisfy our access patterns.
Hi @wtrevena it's been a while since I implemented this so it's not fresh on my mind and I'm unable to share the source.
You're right that the example I provided does not have a @connection in the model. I guess if your models have connections (for example, a doctor can be connected to a prescriptions table), then you would have a ConnectionStack.json.
But if your model does not have any @connections then I guess no need to modify a Connection Stack.
Doctor.json and Patient.json are auto-generated and would be in your build/stacks folder. You would take one of these, cp it into a User.json in your custom stacks folder (stacks/) and find and replace "Patient" with "User". Alternativelly create a temporary User @model then take the auto-generated User.json and move it to your custom stacks folder, then delete the @model.
UserTable.json is not really equivalent the amplify created @model template. It is equivalent to one of the AWS::DynamoDB::Table resources in your cloudformation-template.json (e.g. you would have auto generated DoctorTable and PatientTable dynamodb table resources in your cloudformation-template.json using my example in the gist snippet). It's basically one of these but extracted in its own json.
Interfaces don't work even when they're used as nested properties without an @model
directive. 😢
It will add the property to the return type of queries, but the input types for mutations are missing it. It seems like the feature is there but only half-way. Is this a bug?
So this doesn't work:
type Page @model {
id: ID!
...
blocks: [Block]
}
interface Block {
id: ID!
}
type TextBlock implements Block {
id: ID!
...
}
type ButtonBlock implements Block {
id: ID!
...
}
Same result with unions:
type Page @model {
id: ID!
...
blocks: [Block]
}
type TextBlock {
id: ID!
...
}
type ButtonBlock {
id: ID!
...
}
union Block = TextBlock | ButtonBlock
This is what the generated schema looks like:
type Page {
id: ID!
...
blocks: [Block]
}
input CreatePageInput {
id: ID
# Missing `blocks` field
}
input UpdatePageInput {
id: ID
# Missing `blocks` field
}
hey @ianmartorell did you found how to use the interface without the @model directive?
hey @ianmartorell did you found how to use the interface without the @model directive?
No, what I did was use a discriminator field like this:
enum BlockType {
TEXT
BUTTON
...
}
type Block {
id: ID!
type: BlockType!
... # fields from all block types
}
I am looking forward to support single-table-design for interface @model transformer. I hope this will be supported in the near feature. Thanks a ton!
hey @ianmartorell did you found how to use the interface without the @model directive?
No, what I did was use a discriminator field like this:
enum BlockType { TEXT BUTTON ... } type Block { id: ID! type: BlockType! ... # fields from all block types }
Does anyone have a better solution than this that allows for "type" checking of required data?
Ideally this would do:
type Block {
id: ID!
type: BlockType!
fooRequiredByTypeText: Something! @connection
barRequiredByTypeBlock: SomethingElse! @connection
}
Single table inheritance is what I'm looking for to use a ruby on rails term.
I arrived here for the same reason as @erik-induro. I am trying to use an interface
, with @model
on each of that interface's implement
ations, but am getting this error:
InvalidDirectiveError: Could not find an object type named Node.
Here's my (pseudo)schema:
type Edge @model {
id: ID!
type: EdgeType!
to: Node! @connection(name: "NodeEdges")
from: Node! @connection(name: "NodeEdges")
}
interface Node {
id: ID!
edges: [Edge] @connection(name: "NodeEdges")
}
type PublicNode implements Node
@model
{
# Standard
id: ID!
edges: [Edge] @connection(name: "NodeEdges")
# Custom
content: String
}
type PrivateNode implements Node
@model
{
# Standard
id: ID!
edges: [Edge] @connection(name: "NodeEdges")
# Custom
status: Status
}
I'm currently using amplify mock api
, so I'm not sure if it's a local dev issue or not, just thought I'd ask.
I just want to explain why supporting @model
on interface
/union
types would help me. I'm trying to create two classes of let's say an article:
I would like to be able to control view access to the public on such a node. Is there another way than to use two separate @model
s with different @auth
directives?
This makes sense. Unfortunately, I'm not sure this is going to be supported in the transform out of the box yet but you could write your own transformer that does just this. You can read more about how to do that here: https://github.com/aws-amplify/amplify-cli/blob/master/how-to-write-a-transformer.md
@mikeparisstuff
I'd be interested in going down this route. Are there any in-depth trainings/tutorials for doing so? The docs that you've linked to here are a great introduction, but I'm not smart enough to figure out how to build a plugin from the ground-up using them...
I have the same use case as @loganpowell - I am also not sure how the custom transformer handles this based on the link provided, but also not sure why this wouldn't just be supported out of the box?
I have tried to kludge this with the native Amplify CLI functionality by using a private version of the models I'd like to protect and a public version that can be read by all. However, after using this in our prototype we came up with the following shortcomings:
@manyToMany
). I.e., If I want to create a private node that cannot be reached that has private assets, I have the same problem as number 1 above, but also in a nested field (so the parent also has this issue) leading to a lot of complex client boilerplate that has tripped us up a number of times so far.The benefit of allowing the @model
to live on an interface, IMHO, would allow us to keep the data in the same table, requiring only the change of a sort key (or primary key on a GSI) for that entry. If this was married with the new permissions scoping features in a way that enabled entity permission control based on that key/field, we could have a much simpler client developer experience.
However, I would be happy to implement my own transformer which enabled this if the docs provided such an example.
Edit: Perhaps allowing an enum
to provide the authentication rules might allow us to bypass much of the concerns that modeled interfaces address in the context of the preceding issues, something like this:
enum PermissionType {
PUBLIC @auth( rules: [
{ allow: public, operations: [ read ] },
{ allow: groups, groups: ["Admins", "Editors"] }
])
PRIVATE @auth( rules: [
{ allow: groups, groups: ["Admins", "Editors"] }
])
}
type Resource @model {
id: ID! @primaryKey
status: PermissionType! @index(name: "Resources_by_Status", queryField: "resourcesByStatus")
name: String!
}
type Node @model {
id: ID! @primaryKey
status: PermissionType! @index(name: "Nodes_by_Status", queryField: "nodesByStatus")
resources: [Resource] @hasMany(indexName: "Resources_by_Node", fields: ["id"])
edges: [Edge] @manyToMany(relationName: "EdgeNode")
}
type Edge @model {
id: ID! @primaryKey
nodes: [Node] @manyToMany(relationName: "EdgeNode")
}
Just left a note along these lines to https://github.com/aws-amplify/amplify-cli/issues/1043
related aws-amplify/amplify-category-api#314
@alharris-at As an FYI I'm getting one notification per issue I'm watching as you move them to the new repositories Is the monorepo being split up? Will make it much easier to contribute and trace issues if so
@johnf Not entirely, but we are separating the API category (Rest + GraphQL functionality) as well as all transformers into a standalone repo reflecting the complexity and focus we plan to pay into these on their own moving forward.
The last entry here is over a year but from my tests it still does not work to use union or interfaces. Even on types without "@model" it did not work for me.
bump+
Hopefully one of these days this will be supported.
This is still an issue, I cannot use interfaces because the Inputs will not be generated for the schemas, so it doesn't matter if we can generate the queries, we cannot create any field that uses an interface, no matter if it uses @model or not.
Which Category is your question related to? graphql-transformer
Quick question: I'm curious if the transformer supports interfaces as documented here: https://docs.aws.amazon.com/appsync/latest/devguide/interfaces-and-unions.html
Currently creating the schema for my app, so I guess I will find out soon! :)