Page updated Nov 8, 2023

Advanced Workflows

This section describes different use cases for constructing your own custom GraphQL requests and how to approach it. You may want to construct your own GraphQL request if you want to

  • retrieve only a subset of the data to reduce data transfer
  • retrieve nested objects at a depth that you choose
  • combine multiple operations into a single request
  • send custom headers to your AppSync endpoint

A GraphQL request is automatically generated for you when using AWSAPIPlugin with the existing workflow. For example, if you have a Todo model, a mutation request to save the Todo will look like this:

Future<void> createAndMutateTodo() async { final todo = Todo(name: 'my first todo', description: 'todo description'); final request = ModelMutations.create(todo); final response = await Amplify.API.mutate(request: request).response; safePrint('Response: $response'); }
1Future<void> createAndMutateTodo() async {
2 final todo = Todo(name: 'my first todo', description: 'todo description');
3 final request = ModelMutations.create(todo);
4 final response = await Amplify.API.mutate(request: request).response;
5 safePrint('Response: $response');
6}

Underneath the covers, a request is generated with a GraphQL document and variables and sent to the AppSync service.

{ "query": "mutation createTodo($input: CreateTodoInput!) { createTodo(input: $input) { id name description } }", "variables": "{ "input": { "id": "[UNIQUE-ID]", "name": "my first todo", "description": "todo description" } } }
1{
2 "query": "mutation createTodo($input: CreateTodoInput!) {
3 createTodo(input: $input) {
4 id
5 name
6 description
7 }
8 }",
9 "variables": "{
10 "input": {
11 "id": "[UNIQUE-ID]",
12 "name": "my first todo",
13 "description": "todo description"
14 }
15 }
16}

The different parts of the document are described as follows

  • mutation - the operation type to be performed, other operation types are query and subscription
  • createTodo($input: CreateTodoInput!) - the name and input of the operation.
  • $input: CreateTodoInput! - the input of type CreateTodoInput! referencing the variables containing JSON input
  • createTodo(input: $input) - the mutation operation which takes a variable input from $input
  • the selection set containing id, name, and description are fields specified to be returned in the response

You can learn more about the structure of a request from GraphQL Query Language and AppSync documentation. To test out constructing your own requests, open the AppSync console using amplify console api and navigate to the Queries tab.

Subset of data

The selection set of the document specifies which fields are returned in the response. For example, if you are displaying a view of the Todo without the description, you can construct the document to omit the field. You can learn more about selection sets here.

query getTodo($id: ID!) { getTodo(id: $id) { id name } }
1query getTodo($id: ID!) {
2 getTodo(id: $id) {
3 id
4 name
5 }
6}

The response data will look like this

{ "data": { "getTodo": { "id": "111", "name": "my first todo" } } }
1{
2 "data": {
3 "getTodo": {
4 "id": "111",
5 "name": "my first todo"
6 }
7 }
8}

First, create your own GraphQLRequest

const getTodo = 'getTodo'; const graphQLDocument = '''query GetTodo(\$id: ID!) { $getTodo(id: \$id) { id name } }'''; final getTodoRequest = GraphQLRequest<Todo>( document: graphQLDocument, modelType: Todo.classType, variables: <String, String>{'id': someTodoId}, decodePath: getTodo, );
1const getTodo = 'getTodo';
2const graphQLDocument = '''query GetTodo(\$id: ID!) {
3 $getTodo(id: \$id) {
4 id
5 name
6 }
7}''';
8final getTodoRequest = GraphQLRequest<Todo>(
9 document: graphQLDocument,
10 modelType: Todo.classType,
11 variables: <String, String>{'id': someTodoId},
12 decodePath: getTodo,
13);

The decodePath specifies which part of the response to deserialize to the modelType. You'll need to specify the operation name (as decodePath) and modelType to deserialize the object at "data.getTodo" successfully into a Todo model.

Then, query for the Todo by a todo id:

Future<void> queryTodo(GraphQLRequest<Todo> getTodoRequest) async { final response = await Amplify.API.query(request: getTodoRequest).response; safePrint('Response: $response'); }
1Future<void> queryTodo(GraphQLRequest<Todo> getTodoRequest) async {
2 final response = await Amplify.API.query(request: getTodoRequest).response;
3 safePrint('Response: $response');
4}

Nested Data

If you have a relational model, you can retrieve the nested object by creating a GraphQLRequest with a selection set containing the nested object's fields. For example, in this schema, the Post can contain multiple comments and notes.

enum PostStatus { ACTIVE INACTIVE } type Post @model { id: ID! title: String! rating: Int! status: PostStatus! comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"]) notes: [Note] @hasMany(indexName: "byNote", fields: ["id"]) } type Comment @model { id: ID! postID: ID! @index(name: "byPost", sortKeyFields: ["content"]) post: Post! @belongsTo(fields: ["postID"]) content: String! } type Note @model { id: ID! postID: ID! @index(name: "byNote", sortKeyFields: ["content"]) post: Post! @belongsTo(fields: ["postID"]) content: String! }
1enum PostStatus {
2 ACTIVE
3 INACTIVE
4}
5
6type Post @model {
7 id: ID!
8 title: String!
9 rating: Int!
10 status: PostStatus!
11 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
12 notes: [Note] @hasMany(indexName: "byNote", fields: ["id"])
13}
14
15type Comment @model {
16 id: ID!
17 postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
18 post: Post! @belongsTo(fields: ["postID"])
19 content: String!
20}
21
22type Note @model {
23 id: ID!
24 postID: ID! @index(name: "byNote", sortKeyFields: ["content"])
25 post: Post! @belongsTo(fields: ["postID"])
26 content: String!
27}

If you only want to retrieve the comments, without the notes, create a GraphQLRequest for the Post with nested fields only containing the comment fields.

const getPost = 'getPost'; const graphQLDocument = '''query GetPost(\$id: ID!) { $getPost(id: \$id) { id title rating status comments { items { id postID content } } } }'''; final getPostRequest = GraphQLRequest<Post>( document: graphQLDocument, modelType: Post.classType, variables: <String, String>{'id': somePostId}, decodePath: getPost, );
1const getPost = 'getPost';
2const graphQLDocument = '''query GetPost(\$id: ID!) {
3 $getPost(id: \$id) {
4 id
5 title
6 rating
7 status
8 comments {
9 items {
10 id
11 postID
12 content
13 }
14 }
15 }
16}''';
17final getPostRequest = GraphQLRequest<Post>(
18 document: graphQLDocument,
19 modelType: Post.classType,
20 variables: <String, String>{'id': somePostId},
21 decodePath: getPost,
22);

Then, query for the Post with nested comments included in decoded response:

Future<void> queryPostWithNestedComments( GraphQLRequest<Post> getPostRequest, ) async { final response = await Amplify.API.query(request: getPostRequest).response; safePrint('Response $response'); }
1Future<void> queryPostWithNestedComments(
2 GraphQLRequest<Post> getPostRequest,
3) async {
4 final response = await Amplify.API.query(request: getPostRequest).response;
5 safePrint('Response $response');
6}

Combining multiple GraphQL operations in a single request

GraphQL allows you to run multiple GraphQL operations (queries/mutations) as part of a single network request from the client code. To perform multiple operations in a single request, you can place them within the same GraphQL document. For example, to retrieve a Post and a Todo:

const getTodo = 'getTodo'; const getPost = 'getPost'; const graphQLDocument = ''' query GetPostAndTodo(\$todoId: ID!, \$postId: ID!) { $getTodo(id: \$todoId) { id name } $getPost(id: \$postId) { id title rating } } '''; final multiOperationRequest = GraphQLRequest<String>( document: graphQLDocument, variables: <String, String>{'todoId': someTodoId, 'postId': somePostId}, );
1const getTodo = 'getTodo';
2const getPost = 'getPost';
3const graphQLDocument = '''
4 query GetPostAndTodo(\$todoId: ID!, \$postId: ID!) {
5 $getTodo(id: \$todoId) {
6 id
7 name
8 }
9 $getPost(id: \$postId) {
10 id
11 title
12 rating
13 }
14 }
15
16''';
17final multiOperationRequest = GraphQLRequest<String>(
18 document: graphQLDocument,
19 variables: <String, String>{'todoId': someTodoId, 'postId': somePostId},
20);

Notice here that modelType and decodePath are omitted. When these decoding variables are omitted, the plugin simply returns the result as a raw String from the response.

Once you have the response data in a String, you can parse it using json.decode() and pass the resulting Map to the model's fromJson() method to create an instance of the model.

// Do not forget to add this to imports for json.decode import 'dart:convert'; ... Future<void> queryMultiOperationRequest( GraphQLRequest<String> operation, ) async { final response = await Amplify.API.query(request: multiOperationRequest).response; if (response.data != null) { final jsonData = (json.decode(response.data) as Map).cast<String, Object?>(); final post = Post.fromJson((jsonData[getPost] as Map).cast<String, Object?>); final todo = Todo.fromJson((jsonData[getTodo] as Map).cast<String, Object?>); } }
1// Do not forget to add this to imports for json.decode
2import 'dart:convert';
3
4...
5
6Future<void> queryMultiOperationRequest(
7 GraphQLRequest<String> operation,
8) async {
9 final response =
10 await Amplify.API.query(request: multiOperationRequest).response;
11 if (response.data != null) {
12 final jsonData =
13 (json.decode(response.data) as Map).cast<String, Object?>();
14 final post =
15 Post.fromJson((jsonData[getPost] as Map).cast<String, Object?>);
16 final todo =
17 Todo.fromJson((jsonData[getTodo] as Map).cast<String, Object?>);
18 }
19}

Combining multiple GraphQL requests on the client-side is different than server-side transaction support. To run multiple transactions as a batch operation refer to the Batch Put Custom Resolver example.

Adding Headers to Outgoing Requests

By default, the API plugin includes appropriate authorization headers on your outgoing requests. However, you may have an advanced use case where you wish to send additional request headers to AppSync.

The simplest option for GraphQL requests is to use the headers property of a GraphQLRequest.

Future<void> queryWithCustomHeaders() async { final operation = Amplify.API.query<String>( request: GraphQLRequest( document: graphQLDocumentString, headers: {'customHeader': 'someValue'}, ), ); final response = await operation.response; final data = response.data; safePrint('data: $data'); }
1Future<void> queryWithCustomHeaders() async {
2 final operation = Amplify.API.query<String>(
3 request: GraphQLRequest(
4 document: graphQLDocumentString,
5 headers: {'customHeader': 'someValue'},
6 ),
7 );
8 final response = await operation.response;
9 final data = response.data;
10 safePrint('data: $data');
11}

Another option is to use the baseHttpClient property of the API plugin which can customize headers or otherwise alter HTTP functionality for all HTTP calls.

// First create a custom HTTP client implementation to extend HTTP functionality. class MyHttpRequestInterceptor extends AWSBaseHttpClient { @override Future<AWSBaseHttpRequest> transformRequest( AWSBaseHttpRequest request, ) async { request.headers.putIfAbsent('customHeader', () => 'someValue'); return request; } } // Then you can pass an instance of this client to `baseHttpClient` when you configure Amplify. await Amplify.addPlugins([ AmplifyAPI(baseHttpClient: MyHttpRequestInterceptor()), ]);
1// First create a custom HTTP client implementation to extend HTTP functionality.
2class MyHttpRequestInterceptor extends AWSBaseHttpClient {
3
4 Future<AWSBaseHttpRequest> transformRequest(
5 AWSBaseHttpRequest request,
6 ) async {
7 request.headers.putIfAbsent('customHeader', () => 'someValue');
8 return request;
9 }
10}
11
12// Then you can pass an instance of this client to `baseHttpClient` when you configure Amplify.
13await Amplify.addPlugins([
14 AmplifyAPI(baseHttpClient: MyHttpRequestInterceptor()),
15]);