Skip to content

Commit

Permalink
Update Positions (#213)
Browse files Browse the repository at this point in the history
* Move unused labmda

* Update

* Test

* Fix

* Fix

* Fix

* Fix

* Fix frontend

* Fix

* Add tests

* Fixes

* Fix

---------

Co-authored-by: Eric Bach <[email protected]>
  • Loading branch information
eric-bach and ericbach authored Dec 30, 2024
1 parent d5a1a33 commit 01652a8
Show file tree
Hide file tree
Showing 54 changed files with 942 additions and 3,637 deletions.
12 changes: 11 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,18 @@ X Switch to use ComboBox from budget-tracker tutorial
X Switch to sonner
X Add tests to validate APIs and workflows
X Switch frontend to use NextJS 14 with turbo
X Switch transactionsResolver to use AppSync JS pipeline resolvers

- Ensure positions updates on transactions create/update, add tests to Lambdas
##### Current Task

- Rearchitect updatePositions/updateBalances to use AppSync JS Resolvers
- Ensure updatePositions updates on investment transactions create/update - FAILING, add tests to Lambdas
- Create updateBalances updates on bank transactions create/update

##### Future Task

- create L3 constructs for AppSync CDK
- Update to nodejs 22
- Frontend - remove "Loading..." on screens, remove landing page for login page, improvements to FE

- Events
Expand Down
191 changes: 125 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,78 +487,66 @@ 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 Lambda - Event Handlers
***/

// 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`
);

const updatePositionsFunction = new NodejsFunction(this, 'UpdatePositions', {
runtime: Runtime.NODEJS_18_X,
functionName: `${props.appName}-${props.envName}-UpdatePositions`,
Expand All @@ -496,6 +557,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 +600,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 +636,11 @@ 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, '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;
}
38 changes: 38 additions & 0 deletions backend/src/appsync/Mutation.createBankTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AppSyncIdentityCognito, Context, DynamoDBPutItemRequest, util } from '@aws-appsync/utils';
import { BankTransaction, MutationCreateBankTransactionArgs } from './api/codegen/appsync';

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

const transactionId = util.autoId();
const datetime = util.time.nowISO8601();

return {
operation: 'PutItem',
key: {
pk: util.dynamodb.toDynamoDB(`trans#${transactionId}`),
},
attributeValues: {
entity: util.dynamodb.toDynamoDB('bank-transaction'),
accountId: util.dynamodb.toDynamoDB(ctx.args.input.accountId),
transactionId: util.dynamodb.toDynamoDB(transactionId),
transactionDate: util.dynamodb.toDynamoDB(ctx.args.input.transactionDate),
payee: util.dynamodb.toDynamoDB(ctx.args.input.payee),
category: util.dynamodb.toDynamoDB(ctx.args.input.category),
amount: util.dynamodb.toDynamoDB(ctx.args.input.amount),
userId: util.dynamodb.toDynamoDB((ctx.identity as AppSyncIdentityCognito).username),
createdAt: util.dynamodb.toDynamoDB(datetime),
updatedAt: util.dynamodb.toDynamoDB(datetime),
},
};
}

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

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

return { ...ctx.result, detailType: 'BankTransactionSavedEvent' };
}
Loading

0 comments on commit 01652a8

Please sign in to comment.