aws-amplify / amplify-flutter

A declarative library with an easy-to-use interface for building Flutter applications on AWS.
https://docs.amplify.aws
Apache License 2.0
1.32k stars 247 forks source link

a.customType({}) modelgen issue - Single-Table-Design flutter Amplify-gen2 #4981

Closed printSamuel closed 4 months ago

printSamuel commented 4 months ago

Description

The goal is to develop a Flutter app that uses a single-table design for the backend. Following the recommendations in the documentation, instead of defining the model like this:

SuEItem: a.model({
    ...
})

I do it like:

SuEItem: a.customType({
    ...
})

Using 'a.customType()' ensures that Amplify does not generate resolvers or create a DynamoDB table for this specific model, which is exactly what I want.

Model generation and publishing work without errors. However, when attempting to use the generated model in the flutter frontend, I encounter an issue: the generated model does not extend amplify_core.Model. Therefore I have red lines: final request = ModelMutations.create(todo);

Couldn't infer type parameter 'T'.

Tried to infer 'SuEItem' for 'T' which doesn't work: Type parameter 'T' is declared to extend 'Model' producing 'Model'. The type 'SuEItem' was inferred from: Parameter 'model' declared as 'T' but argument is 'SuEItem'.

Consider passing explicit type argument(s) to the generic.

Should it not extend Model, or is there a misunderstanding or a bug?

Categories

Steps to Reproduce

  1. Create flutter project with amplify gen2

  2. In your resource.ts file add:

    SuEItem: a.customType({
    PK: a.string().required(),
    SK: a.string().required(),
    author: a.string().required(),
    title: a.string(),
    content: a.string(),
    }),
  3. Run npx ampx sandbox --outputs-format dart --outputs-out-dir lib --outputs-version 0

  4. Run npx ampx generate graphql-client-code --format modelgen --model-target dart --out /Users/YOUR-DESIRED-PATH

Screenshots

When using: a.model()

I want

When using: a.customType()

I get

Platforms

Flutter Version

3.22.0

Amplify Flutter Version

2.1.0

Deployment Method

Amplify CLI + Custom Pipeline

Schema

No response

Equartey commented 4 months ago

Hi @printSamuel, I think this is a misunderstanding of how to integrate customType into a backend. Lets walk through how you can achieve your end goal:

The goal is to develop a Flutter app that uses a single-table design for the backend.

The previous approach is flawed, because as you mentioned there is no backend generated to store your custom type.

Amplify maps every Model to a DynamoDB table 1:1. In order to get a single table, with multiple custom types, you should have them live under the same model. This will allow use of the API model helpers, ie ModelMutations, to create GraphQL requests to mutate data. You can find an example of that on our documentation page and here is an example to tie back to your original schema:

a.schema({
  MyModel: a.model({
    SuEItem: a.customType({
       PK: a.string().required(),
       SK: a.string().required(),
       author: a.string().required(),
       title: a.string(),
       content: a.string(),
    }),
    // Other properties & custom types
    foo: a.string(),
  }),
});

In this example, your top level Model becomes MyModel. Which will have a corresponding DynamoDB table created. It will also have your custom type SuEItem as a property, ie MyModel.SuEItem. This property will not have a DynamoDB table created for it. This pattern will enable code gen to create the proper interface mapping for use with ModelMutations.create().

Let me know if that satisfies your use case. We're here to help and answer questions.

printSamuel commented 4 months ago

Thanks for the reply. Overall, I am satisfied with the outcome. However, I won't be using your idea directly. Still, your suggestion provided me with valuable knowledge.

In case anyone is interested in my final solution, here is a step-by-step guide on how I achieved the desired result based on your input:

Initially, your suggestion resulted in this: Screenshot 2024-06-05 at 23 09 57

However, my goal was to achieve something like this: Screenshot 2024-06-05 at 23 04 43

To reach my goal, I deleted the Amplify-generated resolvers and created my own. This approach allowed me to achieve my desired outcome, but it was quite challenging. Every subtype(or model inside MyModel) required another if-else branch, making querying VERY difficult. The main issue was that I couldn't write different resolvers and had to manage everything within one resolver to work with all my subtypes(or models inside MyModel).

Now, I don't use ModelMutations.create() in the frontend. Instead, I make GraphQL queries. This change means I don't need custom types(or models inside MyModel) to be part of the same model, and it's fine if they don't extend amplify_core.Model. I can handle the GraphQL response and convert everything into the Amplify-generated model myself.

My final:

My resouce.ts schema:

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.guest()]),

  Mytesting: a.customType({
    PK: a.string().required(),
    SK: a.string().required(),
    firstname: a.string(),
  }),

//add more models here...

//write custom resolvers
  getMytesting: a
  .query()
  .arguments({
    PK: a.string().required(),
    SK: a.string().required(),
  })
  .returns(a.ref("Mytesting"))
  .authorization(allow => [allow.publicApiKey()])
  .handler(
    a.handler.custom({
      dataSource: "ExternalTableDataSource",
      entry: "./getMytesting.js",
    })
  ),

 //addMytesting...

 //deleteMytesting...

//add resolvers for any other type you might create
});

getMytesting.js:

import * as ddb from "@aws-appsync/utils/dynamodb";

//this code might not be the final you want...
export function request(ctx) {
  return ddb.get({ key: { PK: ctx.args.PK, SK: ctx.args.SK } });
}

export const response = (ctx) => ctx.result;

backend.ts (Note: You need to manually create the DynamoDB table via the dashboard first. I named mine "SingleTable." If you choose a different name, make sure to replace "SingleTable" with your table's name in the following steps..

import { defineBackend, defineData } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { aws_dynamodb } from "aws-cdk-lib";

export const backend = defineBackend({
  auth,
  data,
});

const externalDataSourcesStack = backend.createStack("MyExternalDataSources");

const externalTable = aws_dynamodb.Table.fromTableName(
  externalDataSourcesStack,
  "MyExternalTable",
  "SingleTable"
);

backend.data.addDynamoDbDataSource(
  "ExternalTableDataSource",
  externalTable
);

Frontend-Code:

                  String a = "pmkey";
                  String b = "pmkey";

                  final request = GraphQLRequest(
                  document: '''
  query MyQuery(\$PK: String!, \$SK: String!) {
  getMytesting(PK: \$PK, SK: \$SK) {
    PK
    SK
    firstname
  }
}

  ''',
variables: {'PK': a, 'SK': a},
                );

                final response =
                    await Amplify.API.query(request: request).response;

                // final todos = response.data?.items;
                // if (todos == null) {
                //   safePrint('errors: ${response.errors}');
                // }
                safePrint("response: $response");
                Map<String, dynamic> jsonMap = json.decode(response.data!);
                safePrint("JsonMap: $jsonMap");
                Map<String, dynamic> nestedJson = jsonMap['getMytesting'];
                     //xxx.fromJson method is generated from modelgen
                Mytesting mytest = Mytesting.fromJson(nestedJson);
                safePrint("mytest: $mytest");
                safePrint(mytest.firstname);
              } catch (e) {
                print("Error: $e");
              }