aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
89 stars 79 forks source link

Gen 2 DX for per-resolver caching #2712

Open schisne opened 4 months ago

schisne commented 4 months ago

Describe the feature you'd like to request

Today, in Amplify Gen 2, enabling AppSync per-resolver caching is possible under limited circumstances. The specific use case I'm trying to accomplish is per-resolver caching for a pipeline resolver that fetches a secret from Secrets Manager and then calls an external API using that secret, which I have implemented as a pipeline HTTP resolver with two AppSync JavaScript functions--but as shown below, the limitations are broader than just that use case. I think the developer experience can be improved.

To enable this behavior, one must first define a cache resource for the AppSync API in backend.ts:

new CfnApiCache(backend.data, 'AmplifyGqlApiCache', {
  apiId: backend.data.apiId,
  apiCachingBehavior: 'PER_RESOLVER_CACHING',
  ttl: 60,
  type: 'SMALL',
})

Then per-resolver caching can be defined in one of three ways:

If using a.model

Assuming type MyModel: a.model({ myField: a.string() }) in data/resource.ts, one can enable caching for that field from backend.ts with the following:

backend.data.resources.cfnResources.cfnResolvers['MyModel.myField'].cachingConfig = { ttl: 60 }

By using a.model, one is limited in what kind of resolver is possible for that type. It cannot, for example, be an HTTP resolver or a pipeline resolver, as my use case entails.

If using a.customType

Assuming MyCustomType: a.customType({ myField: a.string() }), example backend.ts code necessary for a pipeline HTTP resolver that retrieves a secret from Secrets Manager and then does something with that secret:

const fetchSecretFunction = backend.data.addFunction('FetchSecretFunction', {
  name: 'fetchSecretFunction',
  dataSource: secretsManagerHttpDataSourceDefinedSeparately,
  code: Code.fromAsset('./fetch-secret.js'),
  runtime: FunctionRuntime.JS_1_0_0
})
const doSomethingFunction = backend.data.addFunction('DoSomethingFunction', {
  name: 'doSomethingFunction',
  dataSource: doSomethingHttpDataSourceDefinedSeparately,
  code: Code.fromAsset('./do-something.js'),
  runtime: FunctionRuntime.JS_1_0_0
})
const myResolver = backend.data.addResolver('MyResolver', {
  typeName: 'MyCustomType',
  fieldName: 'myField',
  pipelineConfig: [fetchSecretFunction, doSomethingFunction],
  code: Code.fromInline(`
    export const request = (ctx) => { return {} }
    export const response = (ctx) => { return ctx.prev.result }
  `),
  runtime: FunctionRuntime.JS_1_0_0,
  cachingConfig: {
    ttl: Duration.minutes(1)
  }
})

In my opinion, this is too verbose. By bringing the entire resolver definition into backend.ts, it also establishes an additional place that a developer must look to find the definitions of the GraphQL types.

If using a.handler.custom

Per-resolver caching appears not to be possible here. But this is where I'd like it to be.

Describe the solution you'd like

First preference

Allow a way to configure per-resolver caching within data/resource.ts for custom types and custom handlers, e.g.:

const schema = a.schema({
  doSomething: a.query().returns(a.string()).handler([
    a.handler.custom({
      dataSource: 'SecretsManagerHttpDataSource',
      entry: '../fetch-secret.js'
    }),
    a.handler.custom({
      dataSource: 'DoSomethingHttpDataSource',
      entry: '../do-something.js'
    })
  ]).cachingConfig({ ttl: 60 })
}).authorization((allow) => [allow.authenticated()])

(cachingConfig being the key part of the above example)

Second preference

Allow a way to reference, from within backend.ts, the resolvers that have been defined in data/resource.ts with either a.handler.custom() or a.customType() notation. Using the same example as above, one might define the caching this way in backend.ts:

backend.data.resources.cfnResources.cfnResolvers['Query.doSomething'].cachingConfig = { ttl: 60 }

Describe alternatives you've considered

Intuitively, one might assume that in the "Second preference" example above, my custom type and/or custom handler would already be present in the backend.data.resources.cfnResources.cfnResolvers object; however, empirically, cfnResovers seems to contain only "models" from the data construct. The way I verified this was by adding an example model, custom type, and custom handler to data/resource.ts (see the examples above) and then adding the keys of cfnResolvers to amplify-outputs.json (the below would be in backend.ts):

backend.addOutput({
  custom: {
    resolvers: Object.keys(backend.data.resources.cfnResources.cfnResolvers)
  }
})

Additional context

No response

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

Would this feature include a breaking change?

AnilMaktala commented 4 months ago

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

dpilch commented 3 months ago

The option you outlined using backend.data.resources.cfnResources is the intended use case for configurations not directly exposed through Amplify. We can keep this issue as a feature request to provide more direct access to the caching config through the data schema builder, but for the time being please use the backend.data.resources.cfnResources method.

schisne commented 3 months ago

The option you outlined using backend.data.resources.cfnResources is the intended use case for configurations not directly exposed through Amplify. We can keep this issue as a feature request to provide more direct access to the caching config through the data schema builder, but for the time being please use the backend.data.resources.cfnResources method.

As mentioned above in "Describe alternatives you've considered," cfnResources does not expose all resolvers--it seems to contain only models, not custom queries nor custom handlers. If I could use cfnResources for this, then my "Second preference" section would already be met, and I could just set the caching config in backend.ts for resolvers I define in the schema. That would be good enough.

As it is, I need to define the entire resolver along with all AppSync functions it uses in backend.ts, totally separate from the schema file, in order to use per-resolver caching for that resolver. That's why I say it's not a great developer experience today.

laranguren-rbi commented 1 week ago

Hey @dpilch reading your answer,

How is the way/solution using backend.data.resources.cfnResources ?

I have the same issue has @schisne, trying the following didn't work:

const resolverToCache =
  backend.data.resources.cfnResources.cfnResolvers['Query.getQuery];

resolverToCache.addPropertyOverride('caching', {
  ttl: 3600, // Cache TTL in seconds (e.g., 1 hour)
  cachingKeys: [
    '$context.arguments.restaurantId',
    '$context.arguments.region',
    '$context.arguments.serviceMode',
  ],
});

May you know @dpilch how to set it?