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

Gen 2 DX for per-resolver caching

Open schisne opened this issue 1 year ago • 5 comments

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?

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

Would this feature include a breaking change?

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

schisne avatar Jul 22 '24 13:07 schisne