AWS Amplify makes the job of authenticating a Flutter App very simple. As we mentioned in our previous post, following the quickstart document for authentication is relatively straightforward. Unfortunately the documentation for connecting to an Existing AWS DynamoDB table is slightly harder to follow as it is incomplete, and none of the examples show how to use a sort key or return multiple rows.
In order to Query an external DynamoDB table from Flutter you will need to combine the steps outlined in the Connect to External DynamoDB (I’ll call this ‘the guide‘) with information from the Custom Queries and Mutations page. I have followed the step numbers in the guide for clarity and to point out the differences and to add in the missing step.
Step1: Create the Table Schema in Flutter
In order to access an existing DynamoDB table from Amplify we need to declare a custom type in the amplify/resources.ts file which maps to the table.
In the example below we have a course module with a primary key of stGroupID and a sort key of stElement. I use table prefix of st for clarity – prefixes are not required but they do prevent using a DynamoDB reserved word as a table or column identifier. The custom type name and the attribute names should match the table name and the column names of your DynamoDB table.
This step effectively the same as the guide with the addition of the sort key.
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
const schema = a.schema({
stModule: a.customType({
stGroupId: a.string().required(),
stElement: a.string().required(),
stTitle: a.string(),
stRef: a.string()
}),
});TSXStep2: Create a datasource to connect to the database
Use Table.fromTableName to get a reference to the table, and add it to the datasource. We will need the data source when we come to query the data. Note if you are using multiple tables, you will need one datasource per table, but only one stack. This step is the same as the guide.
const externalDataSourcesStack = backend.createStack("ExtDataSources");
const dynamoModuleTable = aws_dynamodb.Table.fromTableName(
externalDataSourcesStack,
"extDynamoStudies",
"stModule"
);
backend.data.addDynamoDbDataSource(
"DynamoModuleDataSource",
dynamoModuleTable
);TSXStep 3: Define the Query
Next we need to define the query we are going to run on the table. This is done in the amplify/resources.ts file.
The arguments function passes both the primary and sort keys of our table. Unlike the examples in the guide the query element returns an array of the custom type we defined in the first section, as there may be more than one row of data returned. I have also changed the authorization rule to allow all authenticated app users to call the query.
The handler calls a javascript function which we will define next, and uses the datasource name we defined above for the module table.
getStudy: a
.query()
.arguments({ stGroupId: a.string().required(), stElement: a.string().required() })
.returns(a.ref("stModule").array()) // <-- need to return an array here
.authorization(allow => [allow.authenticated()]) // <-- change auth rules if needed
.handler(
a.handler.custom({
dataSource: "DynamoModuleDataSource",
entry: "./getModule.js",
})
),
TSXStep 4: Build the javascript handler
The handler functions are the resolver functions defined at the bottom of the Connect to External DynamoDB documentation.
I am using a begins_with query on the Sort key, but you can use any of the GraphQL operators to get the data you need here
Particularly note in the response that we are not returning ctx.result as shown in the examples, but ctx.result.items as the query operation can return multiple rows. This maps onto the .array() argument we defined in the query above.
import { util } from "@aws-appsync/utils";
export function request(ctx) {
const { stModuleId, stElement } = ctx.args;
return {
operation: 'Query',
query: {
// expression: 'stModuleId = :mId And stElement = :ele',
expression: 'stModuleId = :mId And begins_with(stElement, :ele)',
expressionValues: util.dynamodb.toMapValues({ ':mId': stModuleId, ':ele': stElement })
},
};
}
export function response(ctx) {
// Return ctx.result.items to map the return array into our data model
return ctx.result.items;
}JavaScriptStep 5:Invoke the query
This section of the guide omits the Dart code that is needed in order to process the results returned by the handler. With a bit of digging it can mostly be found in the Custom Queries and Mutations page.
void getModule(String group, String ele) async {
var graphQLDocument = '''
query GetModule(\$stGroupId: String!, \$stElement: String!) {
getModule(stGroupId: \$stGroupId, stElement: \$stElement) {
stGroupId
stElement
stTitle
stRef
}
}
''';
final getModuleRq = GraphQLRequest<String>(
document: graphQLDocument,
variables: <String, String>{"stGroupId":group, "stElement":ele }
);
final response = await Amplify.API.query(request: getModuleRq).response;
safePrint("**** getModule response $response");
final data = json.decode(response.data!);
// Deal with your data here
// There should be a tsModule.fromJson method in the auto-generated file
// models/stModule.dart
for (Map<String, dynamic> item in data['getModule']) {
tsModule module = tsModule.fromJson(item);
safePrint("**** getModule row ${tsModule.tsTitle}");
// Or you can access the raw data eg
// safePrint("**** getModule row ${item['tsTitle'}");
}
}
DartThe attributes in the graphQLDocument must match the attribute names identified in the model. If they don’t you may get an error similar to the following.
Validation error of type FieldUndefined: Field 'Items' in type 'stModule' is undefined @ 'getModule/Items'BashIf you don’t declare the return type as array() in Step 3 or return ctx.result in step 4 you can end up with the confusing situation where the query is actually working, but you don’t receive any data in the Flutter code.
This may give an error similar to the following (although if you have no required fields in the GraphQLdocument it will give no error at all).
**** getModule response GraphQLResponse<String>: {
"data": "{\"getModule\":null}",
"errors": [
{
"message": "Cannot return null for non-nullable type: 'String' within parent 'stModule' (/getModule/stGroupId)",
"path": [
"getModule",
"stGroupId"
]
},
{
"message": "Cannot return null for non-nullable type: 'String' within parent 'stModule' (/getModule/stElement)",
"path": [
"getModule",
"stElement"
]
}
]
}JSONWrapping up
The final solution is quite simple, but the documentation is lacking. Hopefully this will save someone some of the pain I had in getting to this stage.
If you are still having problems linking Flutter to DynamoDB you can put error calls into the handler code to help debug what is happening.
util.error("My Result", ctx.result);
Leave a Reply