amplify-category-api icon indicating copy to clipboard operation
amplify-category-api copied to clipboard

Allow refering models from custom types in Gen 2 to allow for basic usable argument/return types in custom queries/mutations

Open thomasoehri opened this issue 1 year ago • 8 comments

Describe the feature you'd like to request

I'm integrating OpenSearch in my Gen 2 backend using custom queries where i need to return the items, nextToken as well as the total. The only way of doing this is using a custom type as the return type of the custom query:

// The model i'm working with and trying to integrate search with:
Feedback: a
            .model({
                title: a.string().required(),
                ...
            }),

// The custom type that defines the structure of the return type of the custom search query:
SearchableFeedbackConnection: a.customType({
    items: a.ref("Feedback").array().required(),
    nextToken: a.string(),
    total: a.integer(),
}),

searchFeedback: a
            .query()
            .arguments({
                ...
            })
            // Referencing the custom return type from above:
            .returns(
                a.ref("SearchableFeedbackConnection")
            )

This results in the following error: Cannot use ".ref()" to refer a model from a "custom type". Field "items" of "SearchableFeedbackConnection" refers to model "Feedback"

The problem is that i NEED to return the nextToken alongside all Feedback items. So i can't just return .returns(a.ref("Feedback").array()), which wouldn't result in the error.

The problem isn't isolated to the opensearch example above, i have this issue using custom queries/mutations in general, for example in all my custom list queries where i also need to return the nextToken to be able to query for the next batch of items.

And the same goes for arguments: https://github.com/aws-amplify/amplify-data/issues/451

Describe the solution you'd like

I want to be able to refer to models from custom types so that custom queries work.

Describe alternatives you've considered

Creating another custom type that represents a Feedback instead of using the "Feedback" model obviously doesn't result in the above error but also doesn't have all the field-level authorization rules and relationships that allow for nested queries the model has:

// This does not work since i need all the field-level authorization rules and the ability to perform nested queries using the
// relationships on the "Feedback" model
FeedbackResponse: a.customType({
            ...
        }),

SearchableFeedbackConnection: a.customType({
    items: a.ref("FeedbackResponse").array().required(),
    nextToken: a.string(),
    total: a.integer(),
}),

Additional context

No response

Is this something that you'd be interested in working on?

  • [ ] 👋 I may be able to implement this feature request

Would this feature include a breaking change?

  • [ ] ⚠️ This feature might incur a breaking change

thomasoehri avatar Oct 14 '24 11:10 thomasoehri

This is a blocker for me since my custom list & search queries need to return my model's items but without being able to return a nextToken they're kind of useless atm.

thomasoehri avatar Oct 14 '24 12:10 thomasoehri

Hey @thomasoehri, Thanks for requesting this. We are marking this as a feature request for the team to evaluate further.

AnilMaktala avatar Oct 21 '24 13:10 AnilMaktala

Hi @AnilMaktala, I also encountered this problem. Looking forward to your solutions

1024cainiao avatar Oct 23 '24 03:10 1024cainiao

@AnilMaktala - This is a big blocker for custom queries. There should be a type in the generated schema for the referenced model looks like this:

type ModelMyModelConnection @aws_cognito_user_pools @aws_iam {
  items: [Authorization]!
  nextToken: String
}

How do we get a hold of this object for reference on the TS side of things? Is it nested in the Schema type somewhere?

jmarshall9120 avatar Nov 15 '24 18:11 jmarshall9120

Work around I just implemented for anyone whose blocked by this. The trick is to duplicate the model, and make the dupe a customType. Its annoying but works.

// The model I'm working with and trying to integrate search with:
Feedback: a.model({
  title: a.string().required(),
  ...
}),

// Duplicate that allows referencing:
Feedback2: a.customType({
  title: a.string().required(),
  ...
}),

// The custom type that defines the structure of the return type of the custom search query:
SearchableFeedbackConnection: a.customType({
    items: a.ref("Feedback2").array(),
    nextToken: a.string(),
    scannedCount: a.integer(),
    startedAt: a.timestamp()
}),

searchFeedback: a.query()
    .arguments({
    ...
  })
    // Referencing the custom return type from above:
    .returns(a.ref("SearchableFeedbackConnection"))
    .handler(a.handler.custom({
        dataSource: a.ref('MasterTenantTable'),
        entry: './MyResolverId.js',
    })),

Appsync will often be none the wiser because the schemas match. However you still won't be able to get nextTokens for nested nodes.

jmarshall9120 avatar Nov 15 '24 19:11 jmarshall9120

I think (hope!) this will shortly be addressed by an open PR here: https://github.com/aws-amplify/amplify-data/pull/497 There's also a similar ticket open here: https://github.com/aws-amplify/amplify-data/issues/435

NicholasAllen avatar Jan 29 '25 15:01 NicholasAllen

Unfortunately, the PR I referenced only seems to address the arguments part of this issue (that has now been merged), not the return values. It's frustrating that they have not looked at both at the same time.

NicholasAllen avatar Mar 05 '25 10:03 NicholasAllen

A comment and quick tip: Comment: This is not isolated to OpenSearch. When handling scalable, dynamic, authorization you’re forced to replicate the prebuilt get, list, etc. in your own custom resolvers with integrated authorization where paginated return types are frequently required. So, I look forward to seeing this feature added! :)

Quick tip (building on what jmarshall9120 way saying): Leverage typescript, extract your types! This will help maintainability So in your example:

const FeedbackType = {
  title: a.string().required(),
  parentId: a.id(),
  // ...
}

const schema = a.schema({
  Parent: a.model(/*whatever you want*/),
  Feedback: a.model({
    ...FeedbackType,
    // Still need to extract relationships
    a.hasOne("Parent", "parentId"),
  }),
  BaseFeedback: a.customType(FeedbackType),
  PaginatedFeedback: a.customType({
    feebacks: a.ref("BaseFeedback").array(),
    nextToken: a.string(),
    total: a.integer(),
    // ...
  }),
  searchFeedback: a
    .query()
    .arguments({
        //...
    })
    .returns(
        a.ref("PaginatedFeedback")
    )
    //.(...)
})

Using built in object methods to split up the schema into pieces is also a great practice for handling large schemas. Took me way too long to realize!

OuttaPencil avatar Mar 06 '25 16:03 OuttaPencil