Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model related GraphQL Transform with Key, Connection, and Auth Directives #337

Closed
lawmicha opened this issue Feb 24, 2020 · 5 comments
Closed
Assignees
Labels
api Issues related to the API category datastore Issues related to the DataStore category

Comments

@lawmicha
Copy link
Contributor

lawmicha commented Feb 24, 2020

A graphQL schema is provided by the customer and amplify codegen models will generate the Model classes used by DataStore and API.
When provisioning the API backend, the schema with object types annotated with model directive will a backend with a set of APIs to apply CRUD operations. More details here: https://aws-amplify.github.io/docs/cli-toolchain/graphql#generates

This issue is to test out the different use cases provided by the documentation here https://aws-amplify.github.io/docs/cli-toolchain/graphql#graphql-transform

The focus will be on Model, Key, Connections, and Auth

After doing this excercise, we conclude with

  • a list of issues that we are currently tracking
  • features we want to support
  • document unsupported use cases
  • document use cases which can be supported with, at a minimum, the Document-based GraphQL API (API Plugin's escape hatch). And If possible, use Model-based GraphQL API.
@lawmicha lawmicha added api Issues related to the API category datastore Issues related to the DataStore category labels Feb 24, 2020
@lawmicha lawmicha self-assigned this Feb 24, 2020
@lawmicha
Copy link
Contributor Author

lawmicha commented Feb 24, 2020

Model Directive

Learn more at https://aws-amplify.github.io/docs/cli-toolchain/graphql#model

type Everything @model {
  id: ID!
  string: String
  int: Int
  float: Float
  date: AWSDate
  dateTime: AWSDateTime
  time: AWSTime
  bool: Boolean
  enum: Choice
  array: [String]
  anotherType: AnotherType
}

enum Choice {
  THIS
  THAT
}

type AnotherType {
  name: String!
}
  • Must have id: ID! on all object types?

Enum support/anotherType inside model type

Date support

AWSTimestamp

Follow up

add integration test #314

@lawmicha
Copy link
Contributor Author

lawmicha commented Feb 24, 2020

Key Directive

https://aws-amplify.github.io/docs/cli-toolchain/graphql#key

Add a custom hashKey for the data to be persisted, such as "email" instead of id: ID!.

type Customer @model @key(fields: ["email"]) {
  email: String!
  username: String
}

This is currently not supported due to DataStore needs an id field to be present. (https://github.com/aws-amplify/amplify-cli/issues/3108)

Ideas around doing customer model gen based on developer using API category or DataStore.

Add customer hashKey and second field is sort key

type Order @model @key(fields: ["customerEmail", "createdAt"]) {
    customerEmail: String!
    createdAt: String!
    orderId: ID!
}

this is similar to just having one customer hash key. the query requires both the hashkey and sort key, and mutations are not affected

A named key directive with hashKey, and two fields, making up the composite key

type Item @model
  @key(fields: ["orderId", "status", "createdAt"])
  @key(name: "ByStatus", fields: ["status", "createdAt"], queryField: "itemsByStatus") {
  orderId: ID!
  status: Status!
  createdAt: AWSDateTime!
  name: String!
}
enum Status {
  DELIVERED
  IN_TRANSIT
  PENDING
  UNKNOWN
}

@key(fields: ["orderId", "status", "createdAt"]) generates the composite query

listItems(
  orderId: ID
  statusCreatedAt: ModelItemPrimaryCompositeKeyConditionInput
  filter: ModelItemFilterInput
  limit: Int
  nextToken: String
  sortDirection: ModelSortDirection): ModelItemConnection

@key(name: "ByStatus", fields: ["status", "createdAt"], queryField: "itemsByStatus") generates

itemsByStatus(
  status: Status
  createdAt: ModelStringKeyConditionInput
  sortDirection: ModelSortDirection
  filter: ModelItemFilterInput
  limit: Int
  nextToken: String): ModelItemConnection

A named key directive, which provides a secondary index, and queryField to allow query operation

  • When the key directive is does not include "named" then it's defining the primary indexes. which means there can only be a single key directive that does not include the "named" argument.

  • A named key directive is specified with the queryField argument to define a top level query that can query the secondary index.

  • The secondary index is only created as a sort key if it is specified in the original key directive (without "named" argument).

type SimpleNamed @model 
  @key(fields: ["id", "name"])
  @key(name: "ByName", fields: ["name"], queryField: "simpleByName") {
    id: ID!
    name: String!
    createdAt: AWSDateTime!
}

@key(fields: ["id", "name"]) creates a hash key with id and sort key with name.
@key(name: "ByName", fields: ["name"], queryField: "simpleByName") exposes a query called simplyByName which will then allow a "list" like query by passing in the name.

The queries look like:

// get query
getSimpleNamed(id: ID!name: String!): SimpleNamed
// list query
listSimpleNameds(
  id: ID
  name: ModelStringKeyConditionInput
  filter: ModelSimpleNamedFilterInput
  limit: Int
  nextToken: String
  sortDirection: ModelSortDirection): ModelSimpleNamedConnection
// custom list query called "simplyByName"
simpleByName(
  name: String
  sortDirection: ModelSortDirection // this is new
  filter: ModelSimpleNamedFilterInput
  limit: Int
  nextToken: String): ModelSimpleNamedConnection

corresponding API call would look like:

// get query
Amplify.API.query(Simple.self, byId: id: "") // no way to pass in sort keys like "name", maybe some sort of options to pass in additional identifiers?
or
let document = """
        query getSimple($id:ID!, $name:String!) {
          getSimpleNamed(id: $id, name: $name) {
            id
            name
            createdAt
          }
        }
        """
let variables = ["id": simpleResult.id, "name": simpleResult.name]
let request = GraphQLRequest(document: document, variables: variables, responseType: SimpleNamed.self, decodePath: "getSimpleNamed")
Amplify.API.query(request: request) { (event) in
    switch event {
    case .completed(let result):
        switch result {
        case .failure(let error):
            print(error)
        case .success(let success):
            print(success)
            query.fulfill()
        }
    case .failed(let error):
        print(error)
    default:
        print("default case")
    }
}
wait(for: [query], timeout: 100)

// list query
Amplify.API.query
// simplyByName
Amplify.API.query

// mutate
let simple = SimpleNamed(name: "name1", createdAt: Date())
var simpleResult: SimpleNamed!
Amplify.API.mutate(of: simple, type: .create) { (event) in
    switch event {
    case .completed(let result):
        switch result {
        case .failure(let error):
            print(error)
        case .success(let success):
            print(success)
            simpleResult = success
            completed.fulfill()
        }
    case .failed(let error):
        print(error)
    default:
        print("default case")
    }
}

if we can modelgen at least the class, it would be useful to be able to pass it in as the responseType, even if we need to use the escape hatch

Findings

  • sort direction when specifying sort keys.
  • 3 or more fields on the key creates composite sort keys
  • Model gen has id: ID! as a requirement.
  • Limitation with Model-based GraphQL API which query's byId, no way pass in sort key.
  • When using the GraphQLRequest, it's actually still pretty straight forward as long as the developer has the query document and variables set up to take in the input parameters for the request. one pain point is that decodepath is needed but can actually be inferred by the decoding logic when there is a single key to decode at the data of the graphQL response so we don't actually need to pass decodePath in. One thing that's nice about this is that ampliy codegen model gave us the class to pass in to the GraphQLRequest as responseType.
  • For list query, this may be a little bit different as the responseType needs to be wrapped in the PaginatedList class since response contains a list of the objects wrapped in items
  • Is there a way to improve the calling pattern by using the GraphQLDocument builder classes? first create a GraphQLDocumentBuilder with the model, and decorate it with the queryname, input fields, and selection Set is auto populated. which should be enough

@lawmicha
Copy link
Contributor Author

lawmicha commented Feb 24, 2020

Connection Directive

type Blog @model {
  id: ID!
  name: String!
  posts: [Post] @connection(name: "BlogPosts")
}
type Post @model {
  id: ID!
  title: String!
  blog: Blog @connection(name: "BlogPosts")
  comments: [Comment] @connection(name: "PostComments")
}
type Comment @model {
  id: ID!
  content: String
  post: Post @connection(name: "PostComments")
}

Has one

type Project @model {
  id: ID!
  name: String
  team: Team @connection
}

type Team @model {
  id: ID!
  name: String!
}

what is the flow?

  • create a team
  • create a project, with the team instance that contains the id?

Has One with use of fields

# Custom field on connection

type Project @model {
  id: ID!
  name: String
  teamID: ID!
  team: Team @connection(fields: ["teamID"])
}

type Team @model {
  id: ID!
  name: String!
}
# Has Many

# Belongs to

# Many to Many

type Note @model {
  id: ID!
  title: String!
  editors: [NoteEditor] @connection(keyName: "byNote", fields: ["id"])
}

# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type NoteEditor
  @model(queries: null)
  @key(name: "byNote", fields: ["noteID", "editorID"])
  @key(name: "byEditor", fields: ["editorID", "noteID"]) {
  id: ID!
  noteID: ID!
  editorID: ID!
  note: Note! @connection(fields: ["noteID"])
  editor: User! @connection(fields: ["editorID"])
}

type User @model {
  id: ID!
  username: String!
  posts: [NoteEditor] @connection(keyName: "byEditor", fields: ["id"])
}

related issue to run through:

TODO:
unnamed simple one to one connection

type Project @model {
    id: ID!
    name: String
    team: Team @connection
}
type Team @model {
    id: ID!
    name: String!
}
  • create a Team first, then create a project with that team id via projectTeamId
  • create a project without a team (is this even possible?), create a team, update the project with the projectTeamId

one to many connections

type Post @model {
    id: ID!
    title: String!
    comments: [Comment] @connection
}
type Comment @model {
    id: ID!
    content: String!
}
  • create a post, then create a comment with a postId via postCommentsId to connect the comment to the post
  • create a comment, without a postId, create a post, then update the comment with the postid (maybe this scenario doesn't make sense if the comment requires a postCommentsId when creating.

data access example

Schema from
https://aws-amplify.github.io/docs/cli-toolchain/graphql#data-access-patterns

@lawmicha
Copy link
Contributor Author

lawmicha commented Feb 24, 2020

Auth Directive

Related Issue to run through: #257

@lawmicha
Copy link
Contributor Author

lawmicha commented Feb 25, 2020

Version Directive

related issue:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Issues related to the API category datastore Issues related to the DataStore category
Projects
None yet
Development

No branches or pull requests

1 participant