(aws-appsync): Align `EventApi` JS Resolvers with `GraphqlApi` Resolvers for `MappingTemplate`
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 4.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 83
Description
### Describe the feature
When building real-time applications with `aws_appsync.EventApi`, a common requirement is to pass values synthesized during CDK deployment (e.g., a DynamoDB table name) into the JavaScript resolver code that runs within the AppSync runtime.
Following best practices, developers often prefer separating resolver logic into dedicated `.js` files using `Code.fromAsset`. However, unlike `Code.fromInline`, `Code.fromAsset` cannot directly interpolate CDK values (like `${myTable.tableName}`). The standard mechanism in AppSync for injecting such dynamic values into the resolver context is via **`MappingTemplate`** (VTL) and `ctx.stash`.
Currently, the `EventApi` construct's `addChannelNamespace` method does not appear to support specifying `requestMappingTemplate` or `responseMappingTemplate` when configured with:
1. `Code.fromAsset(...)` for the resolver code.
2. A *direct* data source integration (e.g., `AppSyncDynamoDbDataSource`) in `publishHandlerConfig` or `subscribeHandlerConfig`.
This prevents developers from using the clean `Code.fromAsset` approach combined with the standard `MappingTemplate` mechanism for passing dynamic values, forcing them into less ideal workarounds and creating an inconsistency compared to standard `GraphqlApi` resolvers.
**Current Cumbersome Workaround: `Code.fromInline`**
To pass the table name currently, one must embed the entire JavaScript resolver logic within a potentially very long template literal string inside the CDK stack code, using TypeScript interpolation. This works but has significant drawbacks:
* **Poor Readability/Maintainability:** Large blocks of JS code embedded in TS infrastructure code are hard to read and manage.
* **No Standard Tooling:** Difficult to apply JS linters, formatters, syntax highlighting, or testing frameworks effectively to the embedded code.
* **Violates Separation of Concerns:** Infrastructure code becomes tightly coupled with application logic.
*(The full code example for the `Code.fromInline` workaround is omitted here for brevity, but available in the previous comment if needed for context).*
**Desired Developer Experience: Aligning with `GraphqlApi` Patterns**
The ideal developer experience would align with how resolvers are configured for the standard `aws_appsync.GraphqlApi`. Developers should be able to:
1. Write the JS resolver logic in a separate `my-resolver.js` file.
2. Use `Code.fromAsset` in the CDK stack to reference this file.
3. Utilize the existing **`MappingTemplate`** class ([docs](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_appsync.MappingTemplate.html)) within `addChannelNamespace` to define request/response templates, enabling values (like `tableName`) to be injected into `ctx.stash`.
4. Access these values within `my-resolver.js` via `ctx.stash.tableName`.
**Illustrative Code for Desired DX (If Supported):**
This code shows how developers *would like* to configure `EventApi`'s channel namespace, mirroring patterns available elsewhere in `aws_appsync`.
```typescript
import { Stack, StackProps, App, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
EventApi,
Code,
MappingTemplate, // CDK provides this utility class
ChannelNamespaceOptions, // Interface would need updating
HandlerConfig, // Interface might need updating conditionally
} from 'aws-cdk-lib/aws-appsync';
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { join } from 'path';
import * as fs from 'fs';
import * as path from 'path';
// --- Placeholder src/resolvers/my-asset-resolver.js ---
/*
import { util } from '@aws-appsync/utils';
import * as ddb from '@aws-appsync/utils/dynamodb';
// Ideal: Get table name from stash
const getTable = (ctx) => {
if (!ctx.stash || !ctx.stash.tableName) {
util.error("Table name not found in stash. Mapping template likely missing/incorrect.");
}
return ctx.stash.tableName;
};
export const onPublish = {
request(ctx) {
const TABLE = getTable(ctx); // Use stashed value
const channel = ctx.info.channel.path;
const timestamp = util.time.nowISO8601();
console.log(\`Publishing to table: \${TABLE}, channel: \${channel}\`);
// ... DDB BatchPut logic ...
return ddb.batchPut({ tables: { [TABLE]: [ /* ... items ... * / ] } });
},
response(ctx) {
// ... response logic ...
return [];
}
}
export const onSubscribe = {
request(ctx) {
const TABLE = getTable(ctx); // Use stashed value
// ... request logic ...
// ... DDB Put logic ...
return ddb.put({ table: TABLE, item: { /* ... item ... * / } });
},
response(ctx) {
// ... response logic ...
return null;
}
}
*/
// --- End of placeholder JS ---
// Ensure placeholder directory and file exist for Code.fromAsset
const resolverDir = path.join(__dirname, '../src/resolvers');
const resolverFile = path.join(resolverDir, 'my-asset-resolver.js'); // Different name for clarity
if (!fs.existsSync(resolverDir)) {
fs.mkdirSync(resolverDir, { recursive: true });
}
if (!fs.existsSync(resolverFile)) {
fs.writeFileSync(resolverFile, '// Placeholder content for asset resolver');
}
export class EventApiDesiredDxStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const eventApi = new EventApi(this, 'MyEventApiDesired', {
apiName: 'my-event-api-desired',
});
const myTable = new Table(this, 'MyDesiredTable', {
partitionKey: { name: 'pk', type: AttributeType.STRING },
billingMode: BillingMode.PAY_PER_REQUEST,
removalPolicy: RemovalPolicy.DESTROY,
});
const ddbDataSource = eventApi.addDynamoDbDataSource('MyDdbDsDesired', myTable);
// IDEAL DEVELOPER EXPERIENCE:
eventApi.addChannelNamespace('asset-namespace', {
// Use separate file - clean!
code: Code.fromAsset(resolverFile),
// Define mapping templates directly (similar to GraphqlApi resolvers)
requestMappingTemplate: MappingTemplate.fromString(
// Simple template to pass table name
\`#set(\$ctx.stash.tableName = "\${myTable.tableName}")\\n{}\`
),
// Response template might also be needed depending on direct source interaction
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
// Handler configs likely just need the data source
publishHandlerConfig: {
dataSource: ddbDataSource,
},
subscribeHandlerConfig: {
dataSource: ddbDataSource,
},
});
}
}
const app = new App();
new EventApiDesiredDxStack(app, 'EventApiDesiredDxStack');
```
### Use Case
See above.
### Proposed Solution
_No response_
### Other Information
_No response_
### Acknowledgements
- [ ] I may be able to implement this feature request
- [ ] This feature might incur a breaking change
### AWS CDK Library version (aws-cdk-lib)
2.194.0
### AWS CDK CLI version
2.1013.0
### Environment details (OS name and version, etc.)
Ubuntu 24.04
Contributor guide
Research direction
Start at EventApi.addChannelNamespace and compare its configuration with GraphqlApi resolver patterns. Read the ChannelNamespaceOptions and HandlerConfig entry points mentioned in the issue, along with MappingTemplate and Code.fromAsset usage. Done means asset-based JavaScript resolvers can accept request and response MappingTemplate settings with direct data sources, consistently with the desired EventApi configuration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- api
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100