diff --git a/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/activityTrigger/resource.ts b/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/activityTrigger/resource.ts index 54da2dfcc7c..cb5efbd318e 100644 --- a/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/activityTrigger/resource.ts +++ b/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/activityTrigger/resource.ts @@ -51,6 +51,7 @@ export function applyEscapeHatches(backend: Backend, activity: Table) { backend.activityTrigger.resources.lambda.addEventSource( new DynamoEventSource(activity, { startingPosition: StartingPosition.LATEST, + batchSize: 100, }) ); activity.grantStreamRead(backend.activityTrigger.resources.lambda.role!); diff --git a/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/recorduseractivity/resource.ts b/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/recorduseractivity/resource.ts index a562bc46144..851daa4de1c 100644 --- a/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/recorduseractivity/resource.ts +++ b/amplify-migration-apps/discussions/_snapshot.post.generate/amplify/function/recorduseractivity/resource.ts @@ -48,16 +48,39 @@ export function applyEscapeHatches(backend: Backend, activity: Table) { 'dynamodb:Delete*', 'dynamodb:PartiQLDelete' ); - for (const model of ['Topic', 'Post', 'Comment']) { - const table = backend.data.resources.tables[model]; - backend.recorduseractivity.resources.lambda.addEventSource( - new DynamoEventSource(table, { - startingPosition: StartingPosition.LATEST, - }) - ); - table.grantStreamRead(backend.recorduseractivity.resources.lambda.role!); - table.grantTableListStreams( - backend.recorduseractivity.resources.lambda.role! - ); - } + const tableTopic = backend.data.resources.tables['Topic']; + backend.recorduseractivity.resources.lambda.addEventSource( + new DynamoEventSource(tableTopic, { + startingPosition: StartingPosition.LATEST, + batchSize: 100, + }) + ); + tableTopic.grantStreamRead(backend.recorduseractivity.resources.lambda.role!); + tableTopic.grantTableListStreams( + backend.recorduseractivity.resources.lambda.role! + ); + const tablePost = backend.data.resources.tables['Post']; + backend.recorduseractivity.resources.lambda.addEventSource( + new DynamoEventSource(tablePost, { + startingPosition: StartingPosition.LATEST, + batchSize: 100, + }) + ); + tablePost.grantStreamRead(backend.recorduseractivity.resources.lambda.role!); + tablePost.grantTableListStreams( + backend.recorduseractivity.resources.lambda.role! + ); + const tableComment = backend.data.resources.tables['Comment']; + backend.recorduseractivity.resources.lambda.addEventSource( + new DynamoEventSource(tableComment, { + startingPosition: StartingPosition.LATEST, + batchSize: 100, + }) + ); + tableComment.grantStreamRead( + backend.recorduseractivity.resources.lambda.role! + ); + tableComment.grantTableListStreams( + backend.recorduseractivity.resources.lambda.role! + ); } diff --git a/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/function/function.generator.test.ts b/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/function/function.generator.test.ts index e777b4b24a9..17cd8e51f61 100644 --- a/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/function/function.generator.test.ts +++ b/packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/function/function.generator.test.ts @@ -1069,16 +1069,14 @@ describe('FunctionGenerator', () => { export function applyEscapeHatches(backend: Backend) { backend.myFunc.resources.cfnResources.cfnFunction.functionName = \`myFunc-\${branchName}\`; - for (const model of ['Todo']) { - const table = backend.data.resources.tables[model]; - backend.myFunc.resources.lambda.addEventSource( - new DynamoEventSource(table, { - startingPosition: StartingPosition.LATEST, - }) - ); - table.grantStreamRead(backend.myFunc.resources.lambda.role!); - table.grantTableListStreams(backend.myFunc.resources.lambda.role!); - } + const tableTodo = backend.data.resources.tables['Todo']; + backend.myFunc.resources.lambda.addEventSource( + new DynamoEventSource(tableTodo, { + startingPosition: StartingPosition.LATEST, + }) + ); + tableTodo.grantStreamRead(backend.myFunc.resources.lambda.role!); + tableTodo.grantTableListStreams(backend.myFunc.resources.lambda.role!); } " `); @@ -1264,4 +1262,251 @@ describe('FunctionGenerator', () => { const generator = createFunctionGenerator({ gen1App, backendGenerator, packageJsonGenerator, outputDir }); await expect(generator.plan()).rejects.toThrow("unsupported runtime 'python3.9'"); }); + + it('renders single DynamoDB trigger with custom BatchSize and StartingPosition from CFN', async () => { + const gen1App = await createGen1App({ + providers: { awscloudformation: { StackName: 'amplify-test-main-123456', Region: 'us-east-1' } }, + function: { + myFunc: { + service: 'Lambda', + output: { Name: 'myFunc-main-abc', Arn: 'arn:aws:lambda:us-east-1:123:function:myFunc-main-abc' }, + }, + }, + }); + jest.spyOn(gen1App, 'resourceMetaOutput').mockReturnValue('myFunc-main-abc'); + jest.spyOn(gen1App, 'json').mockReturnValue({ + Resources: { + EventSourceMapping: { + Type: 'AWS::Lambda::EventSourceMapping', + Properties: { + EventSourceArn: { + 'Fn::ImportValue': { + 'Fn::Sub': '${api}:GetAtt:TodoTable:StreamArn', + }, + }, + FunctionName: { Ref: 'LambdaFunction' }, + BatchSize: 50, + StartingPosition: 'TRIM_HORIZON', + }, + }, + }, + }); + jest.spyOn(gen1App, 'file').mockReturnValue('{}'); + jest.spyOn(gen1App, 'fileExists').mockReturnValue(false); + jest.spyOn(gen1App.aws, 'fetchFunctionConfig').mockResolvedValue({ + FunctionName: 'myFunc-main-abc', + Handler: 'index.handler', + Timeout: 3, + MemorySize: 128, + Runtime: 'nodejs18.x', + Environment: { Variables: {} }, + }); + jest.spyOn(gen1App.aws, 'fetchFunctionSchedule').mockResolvedValue(undefined); + + const generator = createFunctionGenerator({ gen1App, backendGenerator, packageJsonGenerator, outputDir }); + const ops = await generator.plan(); + await ops[0].execute(); + + expect(writtenFile('resource.ts')).toMatchInlineSnapshot(` + "import { defineFunction } from '@aws-amplify/backend'; + import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; + import { StartingPosition } from 'aws-cdk-lib/aws-lambda'; + import type { Backend } from '../../backend'; + + const branchName = process.env.AWS_BRANCH ?? 'sandbox'; + + export const myFunc = defineFunction({ + entry: './index.js', + name: \`myFunc-\${branchName}\`, + timeoutSeconds: 3, + memoryMB: 128, + runtime: 18, + }); + + export function applyEscapeHatches(backend: Backend) { + backend.myFunc.resources.cfnResources.cfnFunction.functionName = \`myFunc-\${branchName}\`; + const tableTodo = backend.data.resources.tables['Todo']; + backend.myFunc.resources.lambda.addEventSource( + new DynamoEventSource(tableTodo, { + startingPosition: StartingPosition.TRIM_HORIZON, + batchSize: 50, + }) + ); + tableTodo.grantStreamRead(backend.myFunc.resources.lambda.role!); + tableTodo.grantTableListStreams(backend.myFunc.resources.lambda.role!); + } + " + `); + }); + + it('renders multiple DynamoDB triggers with DIFFERENT BatchSize/StartingPosition per model', async () => { + const gen1App = await createGen1App({ + providers: { awscloudformation: { StackName: 'amplify-test-main-123456', Region: 'us-east-1' } }, + function: { + myFunc: { + service: 'Lambda', + output: { Name: 'myFunc-main-abc', Arn: 'arn:aws:lambda:us-east-1:123:function:myFunc-main-abc' }, + }, + }, + }); + jest.spyOn(gen1App, 'resourceMetaOutput').mockReturnValue('myFunc-main-abc'); + jest.spyOn(gen1App, 'json').mockReturnValue({ + Resources: { + TodoEventSourceMapping: { + Type: 'AWS::Lambda::EventSourceMapping', + Properties: { + EventSourceArn: { + 'Fn::ImportValue': { + 'Fn::Sub': '${api}:GetAtt:TodoTable:StreamArn', + }, + }, + FunctionName: { Ref: 'LambdaFunction' }, + BatchSize: 10, + StartingPosition: 'TRIM_HORIZON', + }, + }, + PostEventSourceMapping: { + Type: 'AWS::Lambda::EventSourceMapping', + Properties: { + EventSourceArn: { + 'Fn::ImportValue': { + 'Fn::Sub': '${api}:GetAtt:PostTable:StreamArn', + }, + }, + FunctionName: { Ref: 'LambdaFunction' }, + BatchSize: 100, + StartingPosition: 'LATEST', + }, + }, + }, + }); + jest.spyOn(gen1App, 'file').mockReturnValue('{}'); + jest.spyOn(gen1App, 'fileExists').mockReturnValue(false); + jest.spyOn(gen1App.aws, 'fetchFunctionConfig').mockResolvedValue({ + FunctionName: 'myFunc-main-abc', + Handler: 'index.handler', + Timeout: 3, + MemorySize: 128, + Runtime: 'nodejs18.x', + Environment: { Variables: {} }, + }); + jest.spyOn(gen1App.aws, 'fetchFunctionSchedule').mockResolvedValue(undefined); + + const generator = createFunctionGenerator({ gen1App, backendGenerator, packageJsonGenerator, outputDir }); + const ops = await generator.plan(); + await ops[0].execute(); + + expect(writtenFile('resource.ts')).toMatchInlineSnapshot(` + "import { defineFunction } from '@aws-amplify/backend'; + import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; + import { StartingPosition } from 'aws-cdk-lib/aws-lambda'; + import type { Backend } from '../../backend'; + + const branchName = process.env.AWS_BRANCH ?? 'sandbox'; + + export const myFunc = defineFunction({ + entry: './index.js', + name: \`myFunc-\${branchName}\`, + timeoutSeconds: 3, + memoryMB: 128, + runtime: 18, + }); + + export function applyEscapeHatches(backend: Backend) { + backend.myFunc.resources.cfnResources.cfnFunction.functionName = \`myFunc-\${branchName}\`; + const tableTodo = backend.data.resources.tables['Todo']; + backend.myFunc.resources.lambda.addEventSource( + new DynamoEventSource(tableTodo, { + startingPosition: StartingPosition.TRIM_HORIZON, + batchSize: 10, + }) + ); + tableTodo.grantStreamRead(backend.myFunc.resources.lambda.role!); + tableTodo.grantTableListStreams(backend.myFunc.resources.lambda.role!); + const tablePost = backend.data.resources.tables['Post']; + backend.myFunc.resources.lambda.addEventSource( + new DynamoEventSource(tablePost, { + startingPosition: StartingPosition.LATEST, + batchSize: 100, + }) + ); + tablePost.grantStreamRead(backend.myFunc.resources.lambda.role!); + tablePost.grantTableListStreams(backend.myFunc.resources.lambda.role!); + } + " + `); + }); + + it('falls back to StartingPosition LATEST when not specified in CFN template', async () => { + const gen1App = await createGen1App({ + providers: { awscloudformation: { StackName: 'amplify-test-main-123456', Region: 'us-east-1' } }, + function: { + myFunc: { + service: 'Lambda', + output: { Name: 'myFunc-main-abc', Arn: 'arn:aws:lambda:us-east-1:123:function:myFunc-main-abc' }, + }, + }, + }); + jest.spyOn(gen1App, 'resourceMetaOutput').mockReturnValue('myFunc-main-abc'); + jest.spyOn(gen1App, 'json').mockReturnValue({ + Resources: { + EventSourceMapping: { + Type: 'AWS::Lambda::EventSourceMapping', + Properties: { + EventSourceArn: { + 'Fn::ImportValue': { + 'Fn::Sub': '${api}:GetAtt:TodoTable:StreamArn', + }, + }, + FunctionName: { Ref: 'LambdaFunction' }, + }, + }, + }, + }); + jest.spyOn(gen1App, 'file').mockReturnValue('{}'); + jest.spyOn(gen1App, 'fileExists').mockReturnValue(false); + jest.spyOn(gen1App.aws, 'fetchFunctionConfig').mockResolvedValue({ + FunctionName: 'myFunc-main-abc', + Handler: 'index.handler', + Timeout: 3, + MemorySize: 128, + Runtime: 'nodejs18.x', + Environment: { Variables: {} }, + }); + jest.spyOn(gen1App.aws, 'fetchFunctionSchedule').mockResolvedValue(undefined); + + const generator = createFunctionGenerator({ gen1App, backendGenerator, packageJsonGenerator, outputDir }); + const ops = await generator.plan(); + await ops[0].execute(); + + expect(writtenFile('resource.ts')).toMatchInlineSnapshot(` + "import { defineFunction } from '@aws-amplify/backend'; + import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; + import { StartingPosition } from 'aws-cdk-lib/aws-lambda'; + import type { Backend } from '../../backend'; + + const branchName = process.env.AWS_BRANCH ?? 'sandbox'; + + export const myFunc = defineFunction({ + entry: './index.js', + name: \`myFunc-\${branchName}\`, + timeoutSeconds: 3, + memoryMB: 128, + runtime: 18, + }); + + export function applyEscapeHatches(backend: Backend) { + backend.myFunc.resources.cfnResources.cfnFunction.functionName = \`myFunc-\${branchName}\`; + const tableTodo = backend.data.resources.tables['Todo']; + backend.myFunc.resources.lambda.addEventSource( + new DynamoEventSource(tableTodo, { + startingPosition: StartingPosition.LATEST, + }) + ); + tableTodo.grantStreamRead(backend.myFunc.resources.lambda.role!); + tableTodo.grantTableListStreams(backend.myFunc.resources.lambda.role!); + } + " + `); + }); }); diff --git a/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.generator.ts b/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.generator.ts index 6a38bafc61b..d4b56cfdf0d 100644 --- a/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.generator.ts +++ b/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.generator.ts @@ -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'; @@ -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; if (res.Type !== 'AWS::Lambda::EventSourceMapping') continue; @@ -260,9 +267,9 @@ 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; } /** @@ -270,11 +277,11 @@ export class FunctionGenerator implements Planner { * CloudFormation template for EventSourceMapping resources that reference * storage table stream ARNs via `Ref: storageStreamArn`. */ - 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; @@ -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 { diff --git a/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.renderer.ts b/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.renderer.ts index cdc726b3cd1..08c97fdcdbd 100644 --- a/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.renderer.ts +++ b/packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.renderer.ts @@ -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; @@ -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 { @@ -227,7 +243,7 @@ export class FunctionRenderer { if (opts.dataTriggerModels.length > 0) { additionalImports['aws-cdk-lib/aws-lambda-event-sources'] = new Set(['DynamoEventSource']); additionalImports['aws-cdk-lib/aws-lambda'] = new Set(['StartingPosition']); - statements.push(createDynamoTrigger(opts.resourceName, opts.dataTriggerModels)); + statements.push(...createDynamoTrigger(opts.resourceName, opts.dataTriggerModels)); } // Storage DynamoDB triggers (standalone tables, not AppSync-managed) @@ -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(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(); @@ -709,94 +725,97 @@ function createUnMappedAuthGrant(funcName: string, actions: readonly string[]): ); } -/** Creates a DynamoDB stream trigger for-of loop. */ -function createDynamoTrigger(functionName: string, models: readonly string[]): 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.createBlock( - [ - factory.createVariableStatement( - [], - factory.createVariableDeclarationList( - [ - factory.createVariableDeclaration( - 'table', - undefined, - undefined, - factory.createElementAccessExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier('backend.data.resources'), - factory.createIdentifier('tables'), - ), - factory.createIdentifier('model'), +/** Creates separate DynamoDB stream trigger statements per model. */ +function createDynamoTrigger(functionName: string, triggers: readonly DetectedDynamoTrigger[]): ts.Statement[] { + const statements: ts.Statement[] = []; + for (const trigger of triggers) { + const tableVar = `table${trigger.name}`; + // const tableX = backend.data.resources.tables["ModelName"] + statements.push( + factory.createVariableStatement( + [], + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + tableVar, + undefined, + undefined, + factory.createElementAccessExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('backend.data.resources'), + factory.createIdentifier('tables'), ), + factory.createStringLiteral(trigger.name), ), - ], - ts.NodeFlags.Const, - ), + ), + ], + ts.NodeFlags.Const, ), - factory.createExpressionStatement( - factory.createCallExpression( + ), + ); + // backend.funcName.resources.lambda.addEventSource(new DynamoEventSource(tableX, { ... })) + statements.push( + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( factory.createPropertyAccessExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(`backend.${functionName}.resources`), - factory.createIdentifier('lambda'), - ), - factory.createIdentifier('addEventSource'), + factory.createIdentifier(`backend.${functionName}.resources`), + factory.createIdentifier('lambda'), ), - undefined, - [ - factory.createNewExpression(factory.createIdentifier('DynamoEventSource'), undefined, [ - factory.createIdentifier('table'), - factory.createObjectLiteralExpression([TS.enumProp('startingPosition', 'StartingPosition', 'LATEST')]), - ]), - ], + factory.createIdentifier('addEventSource'), ), + undefined, + [ + factory.createNewExpression(factory.createIdentifier('DynamoEventSource'), undefined, [ + factory.createIdentifier(tableVar), + factory.createObjectLiteralExpression(createDynamoEventSourceProps(trigger.props)), + ]), + ], ), - factory.createExpressionStatement( - factory.createCallExpression( - factory.createPropertyAccessExpression(factory.createIdentifier('table'), factory.createIdentifier('grantStreamRead')), - undefined, - [ - factory.createNonNullExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(`backend.${functionName}.resources.lambda`), - factory.createIdentifier('role'), - ), + ), + ); + // tableX.grantStreamRead(backend.funcName.resources.lambda.role!) + statements.push( + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier(tableVar), factory.createIdentifier('grantStreamRead')), + undefined, + [ + factory.createNonNullExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(`backend.${functionName}.resources.lambda`), + factory.createIdentifier('role'), ), - ], - ), + ), + ], ), - factory.createExpressionStatement( - factory.createCallExpression( - factory.createPropertyAccessExpression(factory.createIdentifier('table'), factory.createIdentifier('grantTableListStreams')), - undefined, - [ - factory.createNonNullExpression( - factory.createPropertyAccessExpression( - factory.createIdentifier(`backend.${functionName}.resources.lambda`), - factory.createIdentifier('role'), - ), + ), + ); + // tableX.grantTableListStreams(backend.funcName.resources.lambda.role!) + statements.push( + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier(tableVar), factory.createIdentifier('grantTableListStreams')), + undefined, + [ + factory.createNonNullExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(`backend.${functionName}.resources.lambda`), + factory.createIdentifier('role'), ), - ], - ), + ), + ], ), - ], - true, - ), - ); + ), + ); + } + return statements; } /** 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( @@ -810,18 +829,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( @@ -838,7 +856,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( @@ -855,6 +873,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 | 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( diff --git a/reports/report.xml b/reports/report.xml new file mode 100644 index 00000000000..d952d093a98 --- /dev/null +++ b/reports/report.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file