2026-04-13 14:35:47 -04:00
|
|
|
import * as cdk from 'aws-cdk-lib';
|
|
|
|
|
import { Construct } from 'constructs';
|
|
|
|
|
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
|
|
|
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
|
2026-04-30 16:38:54 -04:00
|
|
|
import * as logs from 'aws-cdk-lib/aws-logs';
|
2026-04-13 14:35:47 -04:00
|
|
|
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
|
|
|
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
2026-06-09 12:33:06 -04:00
|
|
|
import * as kms from 'aws-cdk-lib/aws-kms';
|
|
|
|
|
import * as ssm from 'aws-cdk-lib/aws-ssm';
|
2026-04-13 14:35:47 -04:00
|
|
|
import * as events from 'aws-cdk-lib/aws-events';
|
|
|
|
|
import * as targets from 'aws-cdk-lib/aws-events-targets';
|
|
|
|
|
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
|
|
|
|
export interface WorkorderSyncProps {
|
|
|
|
|
region: string;
|
|
|
|
|
kbDocsBucket: s3.Bucket;
|
|
|
|
|
knowledgeBaseId: string;
|
|
|
|
|
dataSourceId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class WorkorderSyncConstruct extends Construct {
|
|
|
|
|
public readonly syncLambda: lambdaNodejs.NodejsFunction;
|
|
|
|
|
|
|
|
|
|
constructor(scope: Construct, id: string, props: WorkorderSyncProps) {
|
|
|
|
|
super(scope, id);
|
|
|
|
|
|
|
|
|
|
// Import existing DynamoDB tables by name (owned by workorder-ingest stack)
|
|
|
|
|
const workOrdersTable = dynamodb.Table.fromTableName(
|
|
|
|
|
this, 'WorkOrdersTable', 'WorkOrders',
|
|
|
|
|
);
|
|
|
|
|
const commentsTable = dynamodb.Table.fromTableName(
|
|
|
|
|
this, 'WorkOrderCommentsTable', 'WorkOrderComments',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Lambda — scans DynamoDB work orders + comments, uploads markdown to S3, triggers KB ingestion
|
|
|
|
|
this.syncLambda = new lambdaNodejs.NodejsFunction(this, 'SyncLambda', {
|
|
|
|
|
functionName: 'seahaven-workorder-sync',
|
|
|
|
|
entry: path.join(__dirname, '../../lambda/workorder-sync/index.ts'),
|
|
|
|
|
runtime: lambda.Runtime.NODEJS_22_X,
|
2026-04-30 16:38:54 -04:00
|
|
|
architecture: lambda.Architecture.ARM_64,
|
|
|
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
2026-04-13 14:35:47 -04:00
|
|
|
memorySize: 512,
|
|
|
|
|
timeout: cdk.Duration.minutes(5),
|
|
|
|
|
environment: {
|
|
|
|
|
WORK_ORDERS_TABLE: workOrdersTable.tableName,
|
|
|
|
|
COMMENTS_TABLE: commentsTable.tableName,
|
|
|
|
|
KB_BUCKET_NAME: props.kbDocsBucket.bucketName,
|
|
|
|
|
KNOWLEDGE_BASE_ID: props.knowledgeBaseId,
|
|
|
|
|
DATA_SOURCE_ID: props.dataSourceId,
|
|
|
|
|
REGION: props.region,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Grant read access to both DynamoDB tables
|
|
|
|
|
workOrdersTable.grantReadData(this.syncLambda);
|
|
|
|
|
commentsTable.grantReadData(this.syncLambda);
|
|
|
|
|
|
2026-06-09 12:33:06 -04:00
|
|
|
// WorkOrders + WorkOrderComments are SSE-encrypted with the shared
|
|
|
|
|
// customer-managed CMK (INFRA-95 / M-3). Because the tables are imported by
|
|
|
|
|
// name (fromTableName), CDK does not know about their encryption key, so
|
|
|
|
|
// grantReadData does NOT add the KMS permissions — they must be granted
|
|
|
|
|
// explicitly or every read fails with kms:Decrypt AccessDenied. The CMK key
|
|
|
|
|
// policy delegates to IAM via kms:ViaService, so this identity grant is what
|
|
|
|
|
// authorizes this cross-stack reader.
|
|
|
|
|
const dynamodbCmk = kms.Key.fromKeyArn(
|
|
|
|
|
this,
|
|
|
|
|
'DynamoDbCmk',
|
|
|
|
|
ssm.StringParameter.valueForStringParameter(this, '/seahaven/dynamodb/cmk-arn'),
|
|
|
|
|
);
|
|
|
|
|
dynamodbCmk.grantDecrypt(this.syncLambda);
|
|
|
|
|
|
2026-04-13 14:35:47 -04:00
|
|
|
// Grant read/write to the KB docs bucket (read to list+delete old, write new)
|
|
|
|
|
props.kbDocsBucket.grantReadWrite(this.syncLambda);
|
|
|
|
|
|
|
|
|
|
// Allow Lambda to start a Bedrock KB ingestion job
|
|
|
|
|
this.syncLambda.addToRolePolicy(
|
|
|
|
|
new iam.PolicyStatement({
|
|
|
|
|
actions: ['bedrock:StartIngestionJob'],
|
|
|
|
|
resources: [
|
|
|
|
|
`arn:aws:bedrock:${props.region}:*:knowledge-base/${props.knowledgeBaseId}`,
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// EventBridge rule — fires daily at 02:00 UTC
|
|
|
|
|
const dailyRule = new events.Rule(this, 'DailySyncRule', {
|
|
|
|
|
ruleName: 'seahaven-workorder-daily-sync',
|
|
|
|
|
description: 'Daily Work Orders → KB sync at 02:00 UTC',
|
|
|
|
|
schedule: events.Schedule.cron({ minute: '0', hour: '2' }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dailyRule.addTarget(new targets.LambdaFunction(this.syncLambda));
|
|
|
|
|
}
|
|
|
|
|
}
|