Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-bach committed Dec 29, 2024
1 parent 1bad027 commit d9ff242
Show file tree
Hide file tree
Showing 27 changed files with 487 additions and 79 deletions.
5 changes: 4 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ X Switch to sonner
X Add tests to validate APIs and workflows
X Switch frontend to use NextJS 14 with turbo

- Ensure positions updates on transactions create/update, add tests to Lambdas
- Switch transactionsResolver to use AppSync JS pipeline resolvers, document architecture
- Ensure updatePositions updates on investment transactions create/update - FAILING, add tests to Lambdas
- Create updateBalances updates on bank transactions create/update
- Update to nodejs 22
- Frontend - remove "Loading..." on screens, remove landing page for login page, improvements to FE

- Events
Expand Down
253 changes: 187 additions & 66 deletions backend/lib/api-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {
FieldLogLevel,
InlineCode,
AuthorizationType,
SchemaFile,
DynamoDbDataSource,
FunctionRuntime,
AppsyncFunction,
Resolver,
Definition,
EventBridgeDataSource,
} from 'aws-cdk-lib/aws-appsync';
import { EventBus, Rule } from 'aws-cdk-lib/aws-events';
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
Expand Down Expand Up @@ -130,7 +130,7 @@ export class ApiStack extends Stack {
name: 'dynamoDBDataSource',
serviceRole: new Role(this, `${props.appName}AppSyncServiceRole`, {
assumedBy: new ServicePrincipal('appsync.amazonaws.com'),
roleName: `${props.appName}-appsync-service-role-${props.envName}`,
roleName: `${props.appName}-appsync-dynamodb-service-role-${props.envName}`,
inlinePolicies: {
name: new PolicyDocument({
statements: [
Expand Down Expand Up @@ -158,6 +158,29 @@ export class ApiStack extends Stack {
}),
});

// AppSync EventBridge DataSource
const eventBridgeDataSource = new EventBridgeDataSource(this, 'EventBridgeDataSource', {
api,
eventBus: eventBus,
description: 'EventBridgeDataSource',
name: 'eventBridgeDataSource',
serviceRole: new Role(this, `${props.appName}AppSyncEventBridgeServiceRole`, {
assumedBy: new ServicePrincipal('appsync.amazonaws.com'),
roleName: `${props.appName}-appsync-event-bridge-service-role-${props.envName}`,
inlinePolicies: {
name: new PolicyDocument({
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['events:PutEvents'],
resources: [eventBus.eventBusArn],
}),
],
}),
},
}),
});

// AppSync JS Resolvers
const createAccountFunction = new AppsyncFunction(this, 'createAccountFunction', {
name: 'createAccount',
Expand Down Expand Up @@ -271,6 +294,56 @@ export class ApiStack extends Stack {
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Query.getSymbols.js')),
runtime: FunctionRuntime.JS_1_0_0,
});
const publishEvent = new AppsyncFunction(this, 'publishEvent', {
name: 'publishEvent',
api: api,
dataSource: eventBridgeDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Event.publishEvent.js')),
runtime: FunctionRuntime.JS_1_0_0,
});

const createBankTransaction = new AppsyncFunction(this, 'createBankTransactionFunction', {
name: 'createBankTransaction',
api: api,
dataSource: dynamoDbDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Mutation.createBankTransaction.js')),
runtime: FunctionRuntime.JS_1_0_0,
});
const updateBankTransaction = new AppsyncFunction(this, 'updateBankTransactionFunction', {
name: 'updateBankTransaction',
api: api,
dataSource: dynamoDbDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Mutation.updateBankTransaction.js')),
runtime: FunctionRuntime.JS_1_0_0,
});
const deleteBankTransaction = new AppsyncFunction(this, 'deleteBankTransactionFunction', {
name: 'deleteBankTransaction',
api: api,
dataSource: dynamoDbDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Mutation.deleteBankTransaction.js')),
runtime: FunctionRuntime.JS_1_0_0,
});
const createInvestmentTransaction = new AppsyncFunction(this, 'createInvestmentTransactionFunction', {
name: 'createInvestmentTransaction',
api: api,
dataSource: dynamoDbDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Mutation.createInvestmentTransaction.js')),
runtime: FunctionRuntime.JS_1_0_0,
});
const updateInvestmentTransaction = new AppsyncFunction(this, 'updateInvestmentTransactionFunction', {
name: 'updateInvestmentTransaction',
api: api,
dataSource: dynamoDbDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Mutation.updateInvestmentTransaction.js')),
runtime: FunctionRuntime.JS_1_0_0,
});
const deleteInvestmentTransaction = new AppsyncFunction(this, 'deleteInvestmentTransactionFunction', {
name: 'deleteInvestmentTransaction',
api: api,
dataSource: dynamoDbDataSource,
code: Code.fromAsset(path.join(__dirname, '../src/appsync/build/Mutation.deleteInvestmentTransaction.js')),
runtime: FunctionRuntime.JS_1_0_0,
});

const passthrough = InlineCode.fromInline(`
// The before step
Expand Down Expand Up @@ -414,74 +487,121 @@ export class ApiStack extends Stack {
pipelineConfig: [getSymbolsFunction],
code: passthrough,
});

/***
*** AWS AppSync - Lambda Resolvers
***/

// AWS ADOT Lambda layer
const adotLayer = LayerVersion.fromLayerVersionArn(
this,
'adotLayer',
`arn:aws:lambda:${Stack.of(this).region}:901920570463:layer:aws-otel-nodejs-amd64-ver-1-18-1:4`
);

// AppSync Lambda DataSource
const transactionsReolverFunction = new NodejsFunction(this, 'TransactionsResolver', {
functionName: `${props.appName}-${props.envName}-TransactionsResolver`,
runtime: Runtime.NODEJS_20_X,
handler: 'handler',
entry: path.resolve(__dirname, '../src/lambda/transactionsResolver/main.ts'),
memorySize: 512,
timeout: Duration.seconds(10),
tracing: Tracing.ACTIVE,
layers: [adotLayer],
environment: {
DATA_TABLE_NAME: dataTable.tableName,
EVENTBUS_NAME: eventBus.eventBusName,
AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-handler',
},
});
transactionsReolverFunction.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['dynamodb:Query', 'dynamodb:PutItem', 'dynamodb:UpdateItem', 'dynamodb:DeleteItem'],
resources: [dataTable.tableArn],
})
);
transactionsReolverFunction.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['events:PutEvents'],
resources: [eventBus.eventBusArn],
})
);
const transactionsResolverDataSource = api.addLambdaDataSource('transactionsDataSource', transactionsReolverFunction, {
name: 'TransactionsLambdaDataSource',
});

// Lambda Resolvers
transactionsResolverDataSource.createResolver('createBankTransactionResolver', {
const createBankTransactionResolver = new Resolver(this, 'createBankTransaction', {
api: api,
typeName: 'Mutation',
fieldName: 'createBankTransaction',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [createBankTransaction, publishEvent],
code: passthrough,
});
transactionsResolverDataSource.createResolver('updateBankTransactionResolver', {
const updateBankTransactionResolver = new Resolver(this, 'updateBankTransaction', {
api: api,
typeName: 'Mutation',
fieldName: 'updateBankTransaction',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [updateBankTransaction, publishEvent],
code: passthrough,
});
transactionsResolverDataSource.createResolver('createInvestmentTransactionResolver', {
const deleteBankTransactionResolver = new Resolver(this, 'deleteBankTransaction', {
api: api,
typeName: 'Mutation',
fieldName: 'deleteBankTransaction',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [deleteBankTransaction, publishEvent],
code: passthrough,
});
const createInvestmentTransactionResolver = new Resolver(this, 'createInvestmentTransaction', {
api: api,
typeName: 'Mutation',
fieldName: 'createInvestmentTransaction',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [createInvestmentTransaction, publishEvent],
code: passthrough,
});
transactionsResolverDataSource.createResolver('updateInvestmentTransactionResolver', {
const updateInvestmentTransactionResolver = new Resolver(this, 'updateInvestmentTransaction', {
api: api,
typeName: 'Mutation',
fieldName: 'updateInvestmentTransaction',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [updateInvestmentTransaction, publishEvent],
code: passthrough,
});
transactionsResolverDataSource.createResolver('deleteTransactionResolver', {
const deleteInvestmentTransactionResolver = new Resolver(this, 'deleteInvestmentTransaction', {
api: api,
typeName: 'Mutation',
fieldName: 'deleteTransaction',
fieldName: 'deleteInvestmentTransaction',
runtime: FunctionRuntime.JS_1_0_0,
pipelineConfig: [deleteInvestmentTransaction, publishEvent],
code: passthrough,
});

/***
*** AWS AppSync - Lambda Resolvers
***/

// AWS ADOT Lambda layer
const adotLayer = LayerVersion.fromLayerVersionArn(
this,
'adotLayer',
`arn:aws:lambda:${Stack.of(this).region}:901920570463:layer:aws-otel-nodejs-amd64-ver-1-18-1:4`
);

// AppSync Lambda DataSource
// const transactionsReolverFunction = new NodejsFunction(this, 'TransactionsResolver', {
// functionName: `${props.appName}-${props.envName}-TransactionsResolver`,
// runtime: Runtime.NODEJS_20_X,
// handler: 'handler',
// entry: path.resolve(__dirname, '../src/lambda/transactionsResolver/main.ts'),
// memorySize: 512,
// timeout: Duration.seconds(10),
// tracing: Tracing.ACTIVE,
// layers: [adotLayer],
// environment: {
// DATA_TABLE_NAME: dataTable.tableName,
// EVENTBUS_NAME: eventBus.eventBusName,
// AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-handler',
// },
// });
// transactionsReolverFunction.addToRolePolicy(
// new PolicyStatement({
// effect: Effect.ALLOW,
// actions: ['dynamodb:Query', 'dynamodb:PutItem', 'dynamodb:UpdateItem', 'dynamodb:DeleteItem'],
// resources: [dataTable.tableArn],
// })
// );
// transactionsReolverFunction.addToRolePolicy(
// new PolicyStatement({
// effect: Effect.ALLOW,
// actions: ['events:PutEvents'],
// resources: [eventBus.eventBusArn],
// })
// );
// const transactionsResolverDataSource = api.addLambdaDataSource('transactionsDataSource', transactionsReolverFunction, {
// name: 'TransactionsLambdaDataSource',
// });
// Lambda Resolvers
// transactionsResolverDataSource.createResolver('createBankTransactionResolver', {
// typeName: 'Mutation',
// fieldName: 'createBankTransaction',
// });
// transactionsResolverDataSource.createResolver('updateBankTransactionResolver', {
// typeName: 'Mutation',
// fieldName: 'updateBankTransaction',
// });
// transactionsResolverDataSource.createResolver('createInvestmentTransactionResolver', {
// typeName: 'Mutation',
// fieldName: 'createInvestmentTransaction',
// });
// transactionsResolverDataSource.createResolver('updateInvestmentTransactionResolver', {
// typeName: 'Mutation',
// fieldName: 'updateInvestmentTransaction',
// });
// transactionsResolverDataSource.createResolver('deleteTransactionResolver', {
// typeName: 'Mutation',
// fieldName: 'deleteTransaction',
// });

/***
*** AWS Lambda - Event Handlers
***/
Expand All @@ -496,6 +616,7 @@ export class ApiStack extends Stack {
tracing: Tracing.ACTIVE,
layers: [adotLayer],
environment: {
REGION: Stack.of(this).region,
DATA_TABLE_NAME: dataTable.tableName,
EVENTBUS_PECUNIARY_NAME: eventBus.eventBusName,
AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-handler',
Expand Down Expand Up @@ -538,17 +659,17 @@ export class ApiStack extends Stack {
*** AWS EventBridge - Event Bus Rules
***/

// EventBus Rule - TransactionSavedEventRule
const transactionSavedEventRule = new Rule(this, 'TransactionSavedEventRule', {
ruleName: `${props.appName}-TransactionSavedEvent-${props.envName}`,
description: 'TransactionSavedEvent',
// EventBus Rule - InvestmentTransactionSavedEventRule
const investmentTransactionSavedRule = new Rule(this, 'InvestmentTransactionSavedEventRule', {
ruleName: `${props.appName}-InvestmentTransactionSavedEvent-${props.envName}`,
description: 'InvestmentTransactionSavedEvent',
eventBus: eventBus,
eventPattern: {
source: ['custom.pecuniary'],
detailType: ['TransactionSavedEvent'],
detailType: ['InvestmentTransactionSavedEvent'],
},
});
transactionSavedEventRule.addTarget(
investmentTransactionSavedRule.addTarget(
new LambdaFunction(updatePositionsFunction, {
//deadLetterQueue: SqsQueue,
maxEventAge: Duration.hours(2),
Expand All @@ -574,14 +695,14 @@ export class ApiStack extends Stack {

// EventBridge
new CfnOutput(this, 'EventBusArn', { value: eventBus.eventBusArn });
new CfnOutput(this, 'TransactionSavedEventRuleArn', {
value: transactionSavedEventRule.ruleArn,
new CfnOutput(this, 'InvestmentTransactionSavedRuleArn', {
value: investmentTransactionSavedRule.ruleArn,
});

// Lambda functions
new CfnOutput(this, 'TransactionsResolverFunctionArn', {
value: transactionsReolverFunction.functionArn,
});
// new CfnOutput(this, 'TransactionsResolverFunctionArn', {
// value: transactionsReolverFunction.functionArn,
// });
new CfnOutput(this, 'UpdatePositionsFunctionArn', {
value: updatePositionsFunction.functionArn,
});
Expand Down
36 changes: 36 additions & 0 deletions backend/src/appsync/Event.publishEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AppSyncIdentityCognito, Context, util } from '@aws-appsync/utils';
import { MutationCreateBankTransactionArgs } from './api/codegen/appsync';

export function request(ctx: Context<MutationCreateBankTransactionArgs>) {
console.log('🔔 PublishEvent Request: ', ctx);

const input = ctx.args.input;
const identity = ctx.identity as AppSyncIdentityCognito;

const eventDetail = {
...input,
userId: identity.username,
};

return {
operation: 'PutEvents',
events: [
{
source: 'custom.pecuniary',
detailType: ctx.prev.result.detailType,
detail: eventDetail,
time: util.time.nowISO8601(),
},
],
};
}

export function response(ctx: Context<MutationCreateBankTransactionArgs>) {
console.log('🔔 PublishEvent Response: ', ctx);

if (ctx.error) {
util.error(ctx.error.message, ctx.error.type, ctx.result);
}

return ctx.prev.result;
}
Loading

0 comments on commit d9ff242

Please sign in to comment.