diff --git a/Gemfile.lock b/Gemfile.lock index d266eb96bec..d3d2951e086 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -241,4 +241,4 @@ DEPENDENCIES tzinfo-data BUNDLED WITH - 1.16.5 + 1.16.6 diff --git a/_config.yml b/_config.yml index ad4edddd3e1..3683d0c54f1 100644 --- a/_config.yml +++ b/_config.yml @@ -169,6 +169,12 @@ category_list: class: aws-icon-ps-60-network-nodes url_external: 'https://marketplace.visualstudio.com/items?itemName=aws-amplify.aws-amplify-vscode' icon: '/images/icons/Misc/Present.svg' + - title: GraphQL Transform + excerpt: Transform simple GraphQL schema files into full application backends. + cta: Read more + class: aws-icon-ps-60-network-nodes + icon: '/images/icons/Misc/Tool.svg' + url: '/js/graphql' - category: Categories title: API Guides excerpt: Add cloud features to your app quickly using our toolchain and declarative APIs. diff --git a/js/graphql.md b/js/graphql.md new file mode 100644 index 00000000000..a40e7113558 --- /dev/null +++ b/js/graphql.md @@ -0,0 +1,1918 @@ +--- +--- +# The GraphQL Transform + +After defining your API using the GraphQL Schema Definition Language (SDL), +you can then use this library to transform it into a fully descriptive +CloudFormation template that implements the API's data model. + +For example you might create the backend for a blog like this: + +``` +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") +} +``` + +> This is just an example. The transform defines more directives such as @auth and @searchable below. + +When used along with tools like the Amplify CLI, the GraphQL Transform simplifies the process of +developing, deploying, and maintaining GraphQL APIs. With it, you define your API using the +[GraphQL Schema Definition Language (SDL)](https://facebook.github.io/graphql/June2018/) and can then use automation to transform it into a fully +descriptive cloudformation template that implements the spec. The transform also provides a framework +through which you can define you own transformers as `@directives` for custom workflows. + +## Quick Start + +Navigate into the root of a JavaScript, iOS, or Android project and run: + +```bash +amplify init +``` + +Follow the wizard to create a new app. After finishing the wizard run: + +```bash +amplify add api + +# Select the graphql option and when asked if you +# have a schema, say No. +# Select one of the default samples. You can change it later. +# Choose to edit the schema and it will open your schema.graphql in your editor. +``` + +You can leave the sample as is or try this schema. + +``` +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") +} +``` + +Once you are happy with your schema, save the file and click enter in your +terminal window. if no error messages are thrown this means the transformation +was successful and you can deploy your new API. + +```bash +amplify push +``` + +Go to AWS CloudFormation to view it. You can also find your project assets in the amplify/backend folder under your API. + +Once the API is finsihed deploying, try going to the AWS AppSync console and +running some of these queries in your new API's query page. + +``` +# Create a blog. Remember the returned id. +# Provide the returned id as the "blogId" variable. +mutation CreateBlog { + createBlog(input: { + name: "My New Blog!" + }) { + id + name + } +} + +# Create a post and associate it with the blog via the "postBlogId" input field. +# Provide the returned id as the "postId" variable. +mutation CreatePost($blogId:ID!) { + createPost(input:{title:"My Post!", postBlogId: $blogId}) { + id + title + blog { + id + name + } + } +} + +# Provide the returned id from the CreateBlog mutation as the "blogId" variable +# in the "variables" pane (bottom left pane) of the query editor: +{ + "blogId": "returned-id-goes-here" +} + +# Create a comment and associate it with the post via the "commentPostId" input field. +mutation CreateComment($postId:ID!) { + createComment(input:{content:"A comment!", commentPostId:$postId}) { + id + content + post { + id + title + blog { + id + name + } + } + } +} + +# Provide the returned id from the CreatePost mutation as the "postId" variable +# in the "variables" pane (bottom left pane) of the query editor: +{ + "postId": "returned-id-goes-here" +} + +# Get a blog, its posts, and its posts' comments. +query GetBlog($blogId:ID!) { + getBlog(id:$blogId) { + id + name + posts(filter: { + title: { + eq: "My Post!" + } + }) { + items { + id + title + comments { + items { + id + content + } + } + } + } + } +} + +# List all blogs, their posts, and their posts' comments. +query ListBlogs { + listBlogs { # Try adding: listBlog(filter: { name: { eq: "My New Blog!" } }) + items { + id + name + posts { # or try adding: posts(filter: { title: { eq: "My Post!" } }) + items { + id + title + comments { # and so on ... + items { + id + content + } + } + } + } + } + } +} +``` + +If you want to update your API, open your project's `backend/api/~apiname~/schema.graphql` file (NOT the one in the `backend/api/~apiname~/build` folder) and edit it in your favorite code editor. You can compile the `backend/api/~apiname~/schema.graphql` by running: + +``` +amplify api gql-compile +``` + +and view the compiled schema output in `backend/api/~apiname~/build/schema.graphql`. + +You can then push updated changes with: + +``` +amplify push +``` + +## Directives + +### @model + +Object types that are annotated with `@model` are top-level entities in the +generated API. Objects annotated with `@model` are stored in Amazon DynamoDB and are +capable of being protected via `@auth`, related to other objects via `@connection`, +and streamed into Amazon Elasticsearch via `@searchable`. You may also apply the +`@versioned` directive to instantly add versioning and conflict detection to a +model type. + +#### Definition + +The following SDL defines the `@model` directive that allows you to easily define +top level object types in your API that are backed by Amazon DynamoDB. + +``` +directive @model( + queries: ModelQueryMap, + mutations: ModelMutationMap +) on OBJECT +input ModelMutationMap { create: String, update: String, delete: String } +input ModelQueryMap { get: String, list: String } +``` + +#### Usage + +Define a GraphQL object type and annotate it with the `@model` directive to store +objects of that type in DynamoDB and automatically configure CRUDL queries and +mutations. + +``` +type Post @model { + id: ID! # id: ID! is a required attribute. + title: String! + tags: [String!]! +} +``` + +You may also override the names of any generated queries and mutations, or remove operations entirely. + +``` +type Post @model(queries: { get: "post" }, mutations: null) { + id: ID! + title: String! + tags: [String!]! +} +``` + +This would create and configure a single query field `post(id: ID!): Post` and +no mutation fields. + +#### Generates + +A single `@model` directive configures the following AWS resources: + +- An Amazon DynamoDB table with 5 read/write units. +- An AWS AppSync DataSource configured to access the table above. +- An AWS IAM role attached to the DataSource that allows AWS AppSync to call the above table on your behalf. +- Up to 8 resolvers (create, update, delete, get, list, onCreate, onUpdate, onDelete) but this is configurable via the `query`, `mutation`, and `subscription` arguments on the `@model` directive. +- Input objects for create, update, and delete mutations. +- Filter input objects that allow you to filter objects in list queries and connection fields. + +This input schema document + +``` +type Post @model { + id: ID! + title: String +} +type MetaData { + category: Category +} +enum Category { comedy news } +``` + +would generate the following schema parts + +``` +type Post { + id: ID! + title: String! + metadata: MetaData +} + +type MetaData { + category: Category +} + +enum Category { + comedy + news +} + +input MetaDataInput { + category: Category +} + +enum ModelSortDirection { + ASC + DESC +} + +type ModelPostConnection { + items: [Post] + nextToken: String +} + +input ModelStringFilterInput { + ne: String + eq: String + le: String + lt: String + ge: String + gt: String + contains: String + notContains: String + between: [String] + beginsWith: String +} + +input ModelIDFilterInput { + ne: ID + eq: ID + le: ID + lt: ID + ge: ID + gt: ID + contains: ID + notContains: ID + between: [ID] + beginsWith: ID +} + +input ModelIntFilterInput { + ne: Int + eq: Int + le: Int + lt: Int + ge: Int + gt: Int + contains: Int + notContains: Int + between: [Int] +} + +input ModelFloatFilterInput { + ne: Float + eq: Float + le: Float + lt: Float + ge: Float + gt: Float + contains: Float + notContains: Float + between: [Float] +} + +input ModelBooleanFilterInput { + ne: Boolean + eq: Boolean +} + +input ModelPostFilterInput { + id: ModelIDFilterInput + title: ModelStringFilterInput + and: [ModelPostFilterInput] + or: [ModelPostFilterInput] + not: ModelPostFilterInput +} + +type Query { + getPost(id: ID!): Post + listPosts(filter: ModelPostFilterInput, limit: Int, nextToken: String): ModelPostConnection +} + +input CreatePostInput { + title: String! + metadata: MetaDataInput +} + +input UpdatePostInput { + id: ID! + title: String + metadata: MetaDataInput +} + +input DeletePostInput { + id: ID +} + +type Mutation { + createPost(input: CreatePostInput!): Post + updatePost(input: UpdatePostInput!): Post + deletePost(input: DeletePostInput!): Post +} + +type Subscription { + onCreatePost: Post @aws_subscribe(mutations: ["createPost"]) + onUpdatePost: Post @aws_subscribe(mutations: ["updatePost"]) + onDeletePost: Post @aws_subscribe(mutations: ["deletePost"]) +} +``` + +### @auth + +Object types that are annotated with `@auth` are protected by a set of authorization +rules. Currently, @auth only supports APIs with Amazon Cognito User Pools enabled. +Types that are annotated with `@auth` must also be annotated with `@model`. + +#### Definition + +``` +# When applied to a type, augments the application with +# owner and group-based authorization rules. +directive @auth(rules: [AuthRule!]!) on OBJECT +input AuthRule { + allow: AuthStrategy! + ownerField: String # defaults to "owner" + identityField: String # defaults to "username" + groupsField: String + groups: [String] + queries: [ModelQuery] + mutations: [ModelMutation] +} +enum AuthStrategy { owner groups } +enum ModelQuery { get list } +enum ModelMutation { create update delete } +``` + +#### Usage + +**Owner Authorization** + +``` +# The simplest case +type Post @model @auth(rules: [{allow: owner}]) { + id: ID! + title: String! +} + +# The long form way +type Post + @model + @auth( + rules: [ + {allow: owner, ownerField: "owner", mutations: [create, update, delete], queries: [get, list]}, + ]) +{ + id: ID! + title: String! + owner: String +} +``` + +Owner authorization specifies that a user can access an object. To +do so, each object has an *ownerField* (by default "owner") that stores ownership information +and is verified in various ways during resolver execution. + +You can use the *queries* and *mutations* arguments to specify which operations are augmented as follows: + +- **get**: If the record's owner is not the same as the logged in user (via `$ctx.identity.username`), throw `$util.unauthorized()`. +- **list**: Filter `$ctx.result.items` for owned items. +- **create**: Inject the logged in user's `$ctx.identity.username` as the *ownerField* automatically. +- **update**: Add conditional update that checks the stored *ownerField* is the same as `$ctx.identity.username`. +- **delete**: Add conditional update that checks the stored *ownerField* is the same as `$ctx.identity.username`. + +You may also apply mutliple ownership rules on a single `@model` type. For example, imagine you have a type **Draft** +that stores unfinished posts for a blog. You might want to allow the **Draft's owner** to create, update, delete, and +read **Draft** objects. However, you might also want the **Draft's editors** to be able to update and read **Draft** objects. +To allow for this use case you could use the following type definition: + +``` +type Draft + @model + @auth(rules: [ + + # Defaults to use the "owner" field. + { allow: owner }, + + # Authorize the update mutation and both queries. Use `queries: null` to disable auth for queries. + { allow: owner, ownerField: "editors", mutations: [update] } + ]) { + id: ID! + title: String! + content: String + owner: String! + editors: [String]! +} +``` + +**Ownership with create mutations** + +The ownership authorization rule tries to make itself as easy as possible to use. One +feature that helps with this is that it will automatically fill ownership fields unless +told explicitly not to do so. To show how this works, lets look at how the create mutation +would work for the **Draft** type above: + +``` +mutation CreateDraft { + createDraft(input: { title: "A new draft" }) { + id + title + owner + editors + } +} +``` + +Let's assume that when I mcall this mutation I am logged in as `someuser@my-domain.com`. The result would be: + +```json +{ + "data": { + "createDraft": { + "id": "...", + "title": "A new draft", + "owner": "someuser@my-domain.com", + "editors": ["someuser@my-domain.com"] + } + } +} +``` + +The `Mutation.createDraft` resolver is smart enough to match your auth rules to attributes +and will fill them in be default. If you do not want the value to be automatically set all +you need to do is include a value for it in your input. For example, to have the resolver +automatically set the **owner** but not the **editors**, you would run this: + +``` +mutation CreateDraft { + createDraft( + input: { + title: "A new draft", + editors: [] + } + ) { + id + title + owner + editors + } +} +``` + +This would return: + +```json +{ + "data": { + "createDraft": { + "id": "...", + "title": "A new draft", + "owner": "someuser@my-domain.com", + "editors": [] + } + } +} +``` + +You can try do the same to **owner** but this will throw an **Unauthorized** exception because you are no longer the owner of the object you are trying to create + +``` +mutation CreateDraft { + createDraft( + input: { + title: "A new draft", + editors: [], + owner: null + } + ) { + id + title + owner + editors + } +} +``` + +To set the owner to null with the current schema, you would still need to be in the editors list: + +``` +mutation CreateDraft { + createDraft( + input: { + title: "A new draft", + editors: ["someuser@my-domain.com"], + owner: null + } + ) { + id + title + owner + editors + } +} +``` + +Would return: + +```json +{ + "data": { + "createDraft": { + "id": "...", + "title": "A new draft", + "owner": null, + "editors": ["someuser@my-domain.com"] + } + } +} +``` + + +**Static Group Authorization** + +Static group authorization allows you to protect `@model` types by restricting access +to a known set of groups. For example, you can allow all **Admin** users to create, +update, delete, get, and list Salary objects. + +``` +type Salary @model @auth(rules: [{allow: groups, groups: ["Admin"]}]) { + id: ID! + wage: Int + currency: String +} +``` + +When calling the GraphQL API, if the user credential (as specified by the resolver's `$ctx.identity`) is not +enrolled in the *Admin* group, the operation will fail. + +To enable advanced authorization use cases, you can layer auth rules to provide specialized functionality. +To show how we might do that, let's expand the **Draft** example we started in the **Owner Authorization** +section above. When we last left off, a **Draft** object could be updated and read by both its owner +and any of its editors and could be created and deleted only by its owner. Let's change it so that +now any member of the "Admin" group can also create, update, delete, and read a **Draft** object. + +``` +type Draft + @model + @auth(rules: [ + + # Defaults to use the "owner" field. + { allow: owner }, + + # Authorize the update mutation and both queries. Use `queries: null` to disable auth for queries. + { allow: owner, ownerField: "editors", mutations: [update] }, + + # Admin users can access any operation. + { allow: groups, groups: ["Admin"] } + ]) { + id: ID! + title: String! + content: String + owner: String! + editors: [String]! +} +``` + +**Dynamic Group Auth** + +``` +# Dynamic group authorization with multiple groups +type Post @model @auth(rules: [{allow: groups, groupsField: "groups"}]) { + id: ID! + title: String + groups: [String] +} + +# Dynamic group authorization with a single group +type Post @model @auth(rules: [{allow: groups, groupsField: "group"}]) { + id: ID! + title: String + group: String +} +``` + +With dynamic group authorization, each record contains an attribute specifying +what groups should be able to access it. Use the *groupsField* argument to +specify which attribute in the underlying data store holds this group +information. To specify that a single group should have access, use a field of +type `String`. To specify that multiple groups should have access, use a field of +type `[String]`. + +Just as with the other auth rules, you can layer dynamic group rules on top of other rules. +Let's again expand the **Draft** example from the **Owner Authorization** and **Static Group Authorization** +sections above. When we last left off editors could update and read objects, owners had full +access, and members of the admin group had full access to **Draft** objects. Now we have a new +requirement where each record should be able to specify an optional list of groups that can read +the draft. This would allow you to share an individual document with an external team, for example. + +``` +type Draft + @model + @auth(rules: [ + + # Defaults to use the "owner" field. + { allow: owner }, + + # Authorize the update mutation and both queries. Use `queries: null` to disable auth for queries. + { allow: owner, ownerField: "editors", mutations: [update] }, + + # Admin users can access any operation. + { allow: groups, groups: ["Admin"] } + + # Each record may specify which groups may read them. + { allow: groups, groupsField: "groupsCanAccess", mutations: [], queries: [get, list] } + ]) { + id: ID! + title: String! + content: String + owner: String! + editors: [String]! + groupsCanAccess: [String]! +} +``` + +With this setup, you could create an object that can be read by the "BizDev" group: + +``` +mutation CreateDraft { + createDraft(input: { + title: "A new draft", + editors: [], + groupsCanAccess: ["BizDev"] + }) { + id + groupsCanAccess + } +} +``` + +And another draft that can be read by the "Marketing" group: + +``` +mutation CreateDraft { + createDraft(input: { + title: "Another draft", + editors: [], + groupsCanAccess: ["Marketing"] + }) { + id + groupsCanAccess + } +} +``` + +#### Generates + +The `@auth` directive will add authorization snippets to any relevant resolver +mapping templates at compile time. Different operations use different methods +of authorization. + +**Owner Authorization** + +``` +type Post @model @auth(rules: [{allow: owner}]) { + id: ID! + title: String! +} +``` + +the generated resolvers would be protected like so: + +- `Mutation.createX`: Verify the requesting user has a valid credential and automatically set the **owner** attribute to equal `$ctx.identity.username`. +- `Mutation.updateX`: Update the condition expression so that the DynamoDB `UpdateItem` operation only succeeds if the record's **owner** attribute equals the caller's `$ctx.identity.username`. +- `Mutation.deleteX`: Update the condition expression so that the DynamoDB `DeleteItem` operation only succeeds if the record's **owner** attribute equals the caller's `$ctx.identity.username`. +- `Query.getX`: In the response mapping template verify that the result's **owner** attribute is the same as the `$ctx.identity.username`. If it is not return null. +- `Query.listX`: In the response mapping template filter the result's **items** such that only items with an **owner** attribute that is the same as the `$ctx.identity.username` are returned. + +**Multie Owner Authorization** + +Work in progress. + +**Static Group Authorization** + +``` +type Post @model @auth(rules: [{allow: groups, groups: ["Admin"]}]) { + id: ID! + title: String! + groups: String +} +``` + +Static group auth is simpler than the others. The generated resolvers would be protected like so: + +- `Mutation.createX`: Verify the requesting user has a valid credential and that `ctx.identity.claims.get("cognito:groups")` contains the **Admin** group. If it does not, fail. +- `Mutation.updateX`: Verify the requesting user has a valid credential and that `ctx.identity.claims.get("cognito:groups")` contains the **Admin** group. If it does not, fail. +- `Mutation.deleteX`: Verify the requesting user has a valid credential and that `ctx.identity.claims.get("cognito:groups")` contains the **Admin** group. If it does not, fail. +- `Query.getX`: Verify the requesting user has a valid credential and that `ctx.identity.claims.get("cognito:groups")` contains the **Admin** group. If it does not, fail. +- `Query.listX`: Verify the requesting user has a valid credential and that `ctx.identity.claims.get("cognito:groups")` contains the **Admin** group. If it does not, fail. + +**Dynamic Group Authorization** + +``` +type Post @model @auth(rules: [{allow: groups, groupsField: "groups"}]) { + id: ID! + title: String! + groups: String +} +``` + +the generated resolvers would be protected like so: + +- `Mutation.createX`: Verify the requesting user has a valid credential and that it contains a claim to atleast one group passed to the query in the `$ctx.args.input.groups` argument. +- `Mutation.updateX`: Update the condition expression so that the DynamoDB `UpdateItem` operation only succeeds if the record's **groups** attribute contains at least one of the caller's claimed groups via `ctx.identity.claims.get("cognito:groups")`. +- `Mutation.deleteX`: Update the condition expression so that the DynamoDB `DeleteItem` operation only succeeds if the record's **groups** attribute contains at least one of the caller's claimed groups via `ctx.identity.claims.get("cognito:groups")` +- `Query.getX`: In the response mapping template verify that the result's **groups** attribute contains at least one of the caller's claimed groups via `ctx.identity.claims.get("cognito:groups")`. +- `Query.listX`: In the response mapping template filter the result's **items** such that only items with a **groups** attribute that contains at least one of the caller's claimed groups via `ctx.identity.claims.get("cognito:groups")`. + + +### @connection + +The `@connection` directive enables you to specify relationships between `@model` object types. +Currently, this supports one-to-one, one-to-many, and many-to-one relationships. You may implement many-to-many relationships +yourself using two one-to-many connections and joining @model type. See the usage section for details. + +#### Definition + +``` +directive @connection(name: String) on FIELD_DEFINITION +``` + +#### Usage + +Relationships are specified by annotating fields on an `@model` object type with +the `@connection` directive. + +**Unnamed Connections** + +In the simplest case, you can define a one-to-one connection: + +``` +type Project @model { + id: ID! + name: String + team: Team @connection +} +type Team @model { + id: ID! + name: String! +} +``` + +After it's transformed, you can create projects with a team as follows: + +``` +mutation CreateProject { + createProject(input: { name: "New Project", projectTeamId: "a-team-id"}) { + id + name + team { + id + name + } + } +} +``` + +> **Note** The **Project.team** resolver is preconfigured to work with the defined connection. + +Likewise, you can make a simple one-to-many connection as follows: + +``` +type Post { + id: ID! + title: String! + comments: [Comment] @connection +} +type Comment { + id: ID! + content: String! +} +``` + +After it's transformed, you can create comments with a post as follows: + +``` +mutation CreateCommentOnPost { + createComment(input: { content: "A comment", postCommentsId: "a-post-id"}) { + id + content + } +} +``` + +> **Note** The postCommentsId field on the input may seem unusual. In the one-to-many case without a provided `name` argument there is only partial information to work with, which results in the unusual name. To fix this, provide a value for the @connection's *name* argument and complete the bi-directional relationship by adding a corresponding @connection field to the **Comment** type. + +**Named Connections** + +The **name** argument specifies a name for the +connection and it's used to create bi-directional relationships that reference +the same underlying foreign key. + +For example, if you wanted your `Post.comments` +and `Comment.post` fields to refer to opposite sides of the same relationship, +you need to provide a name. + +``` +type Post { + id: ID! + title: String! + comments: [Comment] @connection(name: "PostComments") +} +type Comment { + id: ID! + content: String! + post: Post @connection(name: "PostComments") +} +``` + +After it's transformed, create comments with a post as follows: + +``` +mutation CreateCommentOnPost { + createComment(input: { content: "A comment", commentPostId: "a-post-id"}) { + id + content + post { + id + title + comments { + id + # and so on... + } + } + } +} +``` + +**Many-To-Many Connections** + +You can implement many to many yourself using two 1-M @connections and a joining @model. For example: + +``` +type Post @model { + id: ID! + title: String! + editors: [PostEditor] @connection(name: "PostEditors") +} + +# Create a join model and disable queries as you don't need them +# and can query through Post.editors and User.posts +type PostEditor @model(queries: null) { + id: ID! + post: Post! @connection(name: "PostEditors") + editor: User! @connection(name: "UserEditors") +} + +type User @model { + id: ID! + username: String! + posts: [PostEditor] @connection(name: "UserEditors") +} +``` + +You can then create Posts & Users independently and join them in a many-to-many by creating PostEditor objects. In the future we will support more native support for many to many out of the box. The issue is being [tracked on github here](https://github.com/aws-amplify/amplify-cli/issues/91). + +#### Generates + +In order to keep connection queries fast and efficient, the GraphQL transform manages +global secondary indexes (GSIs) on the generated tables on your behalf. In the future we +are investigating using adjacency lists along side GSIs for different use cases that are +connection heavy. + +TODO: Finish docs + + +### @versioned + +The `@versioned` directive adds object versioning and conflict resolution to a type. + +#### Definition + +``` +directive @versioned(versionField: String = "version", versionInput: String = "expectedVersion") on OBJECT +``` + +#### Usage + +Add `@versioned` to a type that is also annotate with `@model` to enable object versioning and conflict detection for a type. + +``` +type Post @model @versioned { + id: ID! + title: String! + version: Int! # <- If not provided, it is added for you. +} +``` + +**Creating a Post automatically sets the version to 1** + +``` +mutation Create { + createPost(input:{ + title:"Conflict detection in the cloud!" + }) { + id + title + version # will be 1 + } +} +``` + +**Updating a Post requires passing the "expectedVersion" which is the object's last saved version** + +> Note: When updating an object, the version number will automatically increment. + +``` +mutation Update($postId: ID!) { + updatePost( + input:{ + id: $postId, + title: "Conflict detection in the cloud is great!", + expectedVersion: 1 + } + ) { + id + title + version # will be 2 + } +} +``` + +**Deleting a Post requires passing the "expectedVersion" which is the object's last saved version** + +``` +mutation Delete($postId: ID!) { + deletePost( + input: { + id: $postId, + expectedVersion: 2 + } + ) { + id + title + version + } +} +``` + +Update and delete operations will fail if the **expectedVersion** does not match the version +stored in DynamoDB. You may change the default name of the version field on the type as well as the name +of the input field via the **versionField** and **versionInput** arguments on the `@versioned` directive. + +#### Generates + +The `@versioned` directive manipulates resolver mapping templates and will store a `version` field in versioned objects. + +### @searchable + +The `@searchable` directive handles streaming the data of an `@model` object type to +Amazon Elasticsearch Service and configures search resolvers that search that information. + +> Note: Support for adding the `@searchable` directive does not yet provide automatic indexing for any existing data to Elasticsearch. View the feature request [here](https://github.com/aws-amplify/amplify-cli/issues/98). + +#### Definition + +``` +# Streams data from DynamoDB to Elasticsearch and exposes search capabilities. +directive @searchable(queries: SearchableQueryMap) on OBJECT +input SearchableQueryMap { search: String } +``` + +#### Usage + +Store posts in Amazon DynamoDB and automatically stream them to Amazon ElasticSearch +via AWS Lambda and connect a searchQueryField resolver. + +``` +type Post @model @searchable { + id: ID! + title: String! + createdAt: String! + updatedAt: String! + upvotes: Int +} +``` + +You may then create objects in DynamoDB that will be automatically streamed to lambda +using the normal `createPost` mutation. + +``` +mutation CreatePost { + createPost(input: { title: "Stream me to Elasticsearch!" }) { + id + title + createdAt + updatedAt + upvotes + } +} +``` + +And then search for posts using a `match` query: + +``` +query SearchPosts { + searchPost(filter: { title: { match: "Stream" }}) { + items { + id + title + } + } +} +``` + +There are multiple `SearchableTypes` generated in the schema, based on the datatype of the fields you specify in the Post type. + +The `filter` parameter in the search queery has a searchable type field that corresponds to the field listed in the Post type. For example, the `title` field of the `filter` object, has the following properties (containing the operators that are applicable to the `string` type): + +* `eq` - which uses the Elasticsearch keyword type to match for the exact term. +* `ne` - this is an iverse operation of `eq`. +* `matchPhrase` - searches using the Elasticsearch's [Match Phrase Query](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-match-query-phrase.html) to filter the documents in the search query. +* `matchPhrasePrefix` - This uses the Elasticsearch's [Match Phrase Prefix Query](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-match-query-phrase-prefix.html) to filter the documents in the search query. +* `multiMatch` - Corresponds to the Elasticsearch [Multi Match Query](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-multi-match-query.html). +* `exists` - Corresponds to the Elasticsearch [Exists Query](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-exists-query.html). +* `wildcard` - Corresponds to the Elasticsearch [Wildcard Query](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-wildcard-query.html). +* `regexp` - Corresponds to the Elasticsearch [Regexp Query](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-regexp-query.html). + + +For example, you can filter using the wildcard expression to search for posts using the following `wildcard` query: + +``` +query SearchPosts { + searchPost(filter: { title: { wildcard: "S*Elasticsearch!" }}) { + items { + id + title + } + } +} +``` + +The above query returns all documents whose `title` begins with `S` and ends with `Elasticsearch!`. + +Moreover you can use the `filter` parameter to pass a nested `and`/`or`/`not` condition. By default, every operation in the filter properties is *AND* ed. You can use the `or` or `not` properties in the `filter` parameter of the search query to override this behavior. Each of these operators (`and`, `or`, `not` properties in the filter object) accepts an array of SearchableTypes which are in turn joined by the corresponding operator. For example, consider the following search query: + +``` +query SearchPosts { + searchPost(filter: { + title: { wildcard: "S*" } + or: [ + { createdAt: { eq: "08/20/2018" } }, + { updatedAt: { eq: "08/20/2018" } } + ] + }) { + items { + id + title + } + } +} +``` + +Assuming you used the `createPost` mutation to create new posts with `title`, `createdAt` and `updatedAt` values, the above search query will return you a list of all `Posts`, whose `title` starts with `S` _and_ have `createdAt` _or_ `updatedAt` value as `08/20/2018`. + +Here is a complete list of searchable operations per GraphQL type supported as of today: + +| GraphQL Type | Searchable Operation | +|-------------:|:-------------| +| String | `ne`, `eq`, `match`, `matchPhrase`, `matchPhrasePrefix`, `multiMatch`, `exists`, `wildcard`, `regexp` | +| Int | `ne`, `gt`, `lt`, `gte`, `lte`, `eq`, `range` | +| Float | `ne`, `gt`, `lt`, `gte`, `lte`, `eq`, `range` | +| Boolean | `eq`, `ne` | + +## S3 Objects + +The GraphQL Transform, Amplify CLI, and Amplify Library make it simple to add complex object +support with Amazon S3 to an application. + +### Basics + +At a minimum the steps to add S3 Object support are as follows: + +**Create a Amazon S3 bucket to hold files via `amplify add storage`.** + +**Create a user pool in Amazon Cognito User Pools via `amplify add auth`.** + +**Create a GraphQL API via `amplify add api` and add the following type definition:** + +``` +type S3Object { + bucket: String! + region: String! + key: String! +} +``` + +**Reference the S3Object type from some `@model` type:** + +``` +type Picture @model @auth(rules: [{allow: owner}]) { + id: ID! + name: String + owner: String + + # Reference the S3Object type from a field. + file: S3Object +} +``` + +The GraphQL Transform handles creating the relevant input types and will store pointers to S3 objects in Amazon DynamoDB. The AppSync SDKs and Amplify library handle uploading the files to S3 transparently. + +**Run a mutation with s3 objects from your client app:** + +``` +mutation ($input: CreatePictureInput!) { + createPicture(input: $input) { + id + name + visibility + owner + createdAt + file { + region + bucket + key + } + } +} +``` + +### Tutorial (S3 & React) + +**First create an amplify project:** + +``` +amplify init +``` + +**Next add the `auth` category to enable Amazon Cognito User Pools:** + +``` +amplify add auth + +# You may use the default settings. +``` + +**Then add the `storage` category and configure an Amazon S3 bucket to store files.** + +``` +amplify add storage + +# Select "Content (Images, audio, video, etc.)" +# Follow the rest of the instructions and customize as necessary. +``` + +**Next add the `api` category and configure a GraphQL API with Amazon Cognito User Pools enabled.** + +``` +amplify add api + +# Select the graphql option and then Amazon Cognito User Pools option. +# When asked if you have a schema, say No. +# Select one of the default samples. You can change it later. +# Choose to edit the schema and it will open your schema.graphql in your editor. +``` + +**Once your `schema.graphql` is open in your editor of choice, enter the following:** + +``` +type Picture @model @auth(rules: [{allow: owner}]) { + id: ID! + name: String + owner: String + visibility: Visibility + file: S3Object + createdAt: String +} + +type S3Object { + bucket: String! + region: String! + key: String! +} + +enum Visibility { + public + private +} +``` + +**After defining your API's schema.graphql deploy it to AWS.** + +``` +amplify push +``` + +**In your top level `App.js` (or similar), instantiate the AppSync client and include +the necessary `` and `` components.** + +```javascript +import React, { Component } from 'react'; +import Amplify, { Auth } from 'aws-amplify'; +import { withAuthenticator } from 'aws-amplify-react'; +import AWSAppSyncClient from "aws-appsync"; +import { Rehydrated } from 'aws-appsync-react'; +import { ApolloProvider } from 'react-apollo'; +import awsconfig from './aws-exports'; + +// Amplify init +Amplify.configure(awsconfig); + +const GRAPHQL_API_REGION = awsconfig.aws_appsync_region +const GRAPHQL_API_ENDPOINT_URL = awsconfig.aws_appsync_graphqlEndpoint +const S3_BUCKET_REGION = awsconfig.aws_user_files_s3_bucket_region +const S3_BUCKET_NAME = awsconfig.aws_user_files_s3_bucket +const AUTH_TYPE = awsconfig.aws_appsync_authenticationType + +// AppSync client instantiation +const client = new AWSAppSyncClient({ + url: GRAPHQL_API_ENDPOINT_URL, + region: GRAPHQL_API_REGION, + auth: { + type: AUTH_TYPE, + // Get the currently logged in users credential from + // Amazon Cognito User Pools. + jwtToken: async () => ( + await Auth.currentSession()).getAccessToken().getJwtToken(), + }, + // Uses Amazon IAM credentials to authorize requests to S3. + complexObjectsCredentials: () => Auth.currentCredentials(), +}); + +// Define you root app component +class App extends Component { + render() { + // ... your code here + } +} + +const AppWithAuth = withAuthenticator(App, true); + +export default () => ( + + + + + +); +``` + +**Then define a component and call a mutation to create a Picture object and upload +a file.** + +```javascript +import React, { Component } from 'react'; +import gql from 'graphql-tag'; + +// Define your component +class AddPhoto extends Component { + render() { +