Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { Planner } from '../../../_common/planner';
import { BackendGenerator } from '../backend.generator';
import { Gen1App, DiscoveredResource } from '../../../_common/gen1-app';
import { TS } from '../../ts';
import { FunctionRenderer, FunctionRenderOptions, classifyEnvVars, DynamicEnvVar } from './function.renderer';
import {
FunctionRenderer,
FunctionRenderOptions,
classifyEnvVars,
DynamicEnvVar,
DetectedDynamoTrigger,
extractDynamoTriggerProps,
} from './function.renderer';
import { RootPackageJsonGenerator } from '../../package.json.generator';
import { AuthPermissions } from '../auth/auth.renderer';
import { AuthGenerator } from '../auth/auth.generator';
Expand Down Expand Up @@ -247,10 +254,10 @@ export class FunctionGenerator implements Planner {
return Array.from(new Set(args));
}

private detectDataTriggerModels(): string[] {
private detectDataTriggerModels(): DetectedDynamoTrigger[] {
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const models: string[] = [];
const triggers: DetectedDynamoTrigger[] = [];
for (const resource of Object.values(template.Resources ?? {})) {
const res = resource as Record<string, unknown>;
if (res.Type !== 'AWS::Lambda::EventSourceMapping') continue;
Expand All @@ -260,21 +267,21 @@ export class FunctionGenerator implements Planner {
const fnSub = fnImportValue?.['Fn::Sub'];
if (!fnSub) continue;
const match = fnSub.match(/:GetAtt:(\w+)Table:StreamArn/);
if (match) models.push(match[1]);
if (match) triggers.push({ name: match[1], props: extractDynamoTriggerProps(props) });
}
return models;
return triggers;
}

/**
* Detects storage DynamoDB table triggers by parsing this function's
* CloudFormation template for EventSourceMapping resources that reference
* storage table stream ARNs via `Ref: storage<tableName>StreamArn`.
*/
private detectDynamoTriggerTables(): string[] {
private detectDynamoTriggerTables(): DetectedDynamoTrigger[] {
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped CloudFormation template
const template = this.gen1App.json(templatePath);
const tables: string[] = [];
const triggers: DetectedDynamoTrigger[] = [];

for (const resource of Object.values(template.Resources)) {
const res = resource as Record<string, unknown>;
Expand All @@ -286,11 +293,11 @@ export class FunctionGenerator implements Planner {

const match = eventSourceArn.Ref.match(/^storage(\w+)StreamArn$/);
if (match) {
tables.push(match[1]);
triggers.push({ name: match[1], props: extractDynamoTriggerProps(props) });
}
}

return tables;
return triggers;
}

private isKinesisTrigger(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ const factory = ts.factory;
* Options for rendering a complete function resource.ts file,
* including defineFunction() and applyEscapeHatches().
*/
/**
* Event source mapping properties extracted from a Gen1 CFN template.
*/
export interface DynamoTriggerProps {
readonly startingPosition: string;
readonly batchSize?: number;
}

/**
* A DynamoDB stream trigger with its CFN event source mapping properties.
*/
export interface DetectedDynamoTrigger {
readonly name: string;
readonly props: DynamoTriggerProps;
}

export interface FunctionRenderOptions {
readonly resourceName: string;
readonly entry: string;
Expand All @@ -21,10 +37,10 @@ export interface FunctionRenderOptions {
readonly dynamicEnvVars: readonly DynamicEnvVar[];
readonly dynamoActions: readonly string[];
readonly appSyncPermissions: { readonly hasMutation: boolean; readonly hasQuery: boolean };
readonly dataTriggerModels: readonly string[];
readonly dataTriggerModels: readonly DetectedDynamoTrigger[];
readonly kinesisConfig?: KinesisConfig;
readonly unMappedAuthActions: readonly string[];
readonly storageTriggerTables: readonly string[];
readonly storageTriggerTables: readonly DetectedDynamoTrigger[];
}

export interface KinesisConfig {
Expand Down Expand Up @@ -271,7 +287,7 @@ export class FunctionRenderer {
// Add storage table parameters for standalone DynamoDB tables referenced in the body.
// Collect from both env-var escape hatches and storage triggers.
const allStorageTableParams = new Set<string>(storageTableNames);
for (const t of opts.storageTriggerTables) allStorageTableParams.add(t);
for (const t of opts.storageTriggerTables) allStorageTableParams.add(t.name);
if (allStorageTableParams.size > 0) {
if (!additionalImports['aws-cdk-lib/aws-dynamodb']) {
additionalImports['aws-cdk-lib/aws-dynamodb'] = new Set();
Expand Down Expand Up @@ -710,14 +726,14 @@ function createUnMappedAuthGrant(funcName: string, actions: readonly string[]):
}

/** Creates a DynamoDB stream trigger for-of loop. */
function createDynamoTrigger(functionName: string, models: readonly string[]): ts.ForOfStatement {
function createDynamoTrigger(functionName: string, triggers: readonly DetectedDynamoTrigger[]): ts.ForOfStatement {
return factory.createForOfStatement(
undefined,
factory.createVariableDeclarationList(
[factory.createVariableDeclaration('model', undefined, undefined, undefined)],
ts.NodeFlags.Const,
),
factory.createArrayLiteralExpression(models.map((model) => factory.createStringLiteral(model))),
factory.createArrayLiteralExpression(triggers.map((t) => factory.createStringLiteral(t.name))),
factory.createBlock(
[
factory.createVariableStatement(
Expand Down Expand Up @@ -753,7 +769,7 @@ function createDynamoTrigger(functionName: string, models: readonly string[]): t
[
factory.createNewExpression(factory.createIdentifier('DynamoEventSource'), undefined, [
factory.createIdentifier('table'),
factory.createObjectLiteralExpression([TS.enumProp('startingPosition', 'StartingPosition', 'LATEST')]),
factory.createObjectLiteralExpression(createDynamoEventSourceProps(triggers[0]?.props)),
]),
],
),
Expand Down Expand Up @@ -793,10 +809,9 @@ function createDynamoTrigger(functionName: string, models: readonly string[]): t
}

/** Creates storage DynamoDB stream triggers for standalone tables. */
function createStorageDynamoTrigger(functionName: string, tableNames: readonly string[]): ts.Statement[] {
function createStorageDynamoTrigger(functionName: string, triggers: readonly DetectedDynamoTrigger[]): ts.Statement[] {
const statements: ts.Statement[] = [];
for (const tableName of tableNames) {
// backend.funcName.resources.lambda.addEventSource(new DynamoEventSource(tableName, { startingPosition: StartingPosition.LATEST }))
for (const trigger of triggers) {
statements.push(
factory.createExpressionStatement(
factory.createCallExpression(
Expand All @@ -810,18 +825,17 @@ function createStorageDynamoTrigger(functionName: string, tableNames: readonly s
undefined,
[
factory.createNewExpression(factory.createIdentifier('DynamoEventSource'), undefined, [
factory.createIdentifier(tableName),
factory.createObjectLiteralExpression([TS.enumProp('startingPosition', 'StartingPosition', 'LATEST')]),
factory.createIdentifier(trigger.name),
factory.createObjectLiteralExpression(createDynamoEventSourceProps(trigger.props)),
]),
],
),
),
);
// tableName.grantStreamRead(backend.funcName.resources.lambda.role!)
statements.push(
factory.createExpressionStatement(
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier(tableName), factory.createIdentifier('grantStreamRead')),
factory.createPropertyAccessExpression(factory.createIdentifier(trigger.name), factory.createIdentifier('grantStreamRead')),
undefined,
[
factory.createNonNullExpression(
Expand All @@ -838,7 +852,7 @@ function createStorageDynamoTrigger(functionName: string, tableNames: readonly s
statements.push(
factory.createExpressionStatement(
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier(tableName), factory.createIdentifier('grantTableListStreams')),
factory.createPropertyAccessExpression(factory.createIdentifier(trigger.name), factory.createIdentifier('grantTableListStreams')),
undefined,
[
factory.createNonNullExpression(
Expand All @@ -855,6 +869,30 @@ function createStorageDynamoTrigger(functionName: string, tableNames: readonly s
return statements;
}

/**
* Extracts DynamoDB event source mapping properties (StartingPosition
* and BatchSize) from a CFN EventSourceMapping resource's Properties.
* Falls back to LATEST if StartingPosition is missing.
*/
export function extractDynamoTriggerProps(props: Record<string, unknown> | undefined): DynamoTriggerProps {
const startingPosition = typeof props?.StartingPosition === 'string' ? props.StartingPosition : 'LATEST';
const batchSize = typeof props?.BatchSize === 'number' ? props.BatchSize : undefined;
return { startingPosition, batchSize };
}

/**
* Builds the object literal properties for a DynamoEventSource constructor:
* `{ startingPosition: StartingPosition.X, batchSize: N }`.
*/
function createDynamoEventSourceProps(props: DynamoTriggerProps | undefined): ts.PropertyAssignment[] {
const position = props?.startingPosition ?? 'LATEST';
const assignments: ts.PropertyAssignment[] = [TS.enumProp('startingPosition', 'StartingPosition', position)];
if (props?.batchSize !== undefined) {
assignments.push(factory.createPropertyAssignment('batchSize', factory.createNumericLiteral(props.batchSize)));
}
return assignments;
}

/** Creates a Kinesis stream trigger. */
function createKinesisTrigger(functionName: string): ts.Statement[] {
const fromStreamArn = factory.createVariableStatement(
Expand Down
Loading