From 84d405d2eaf1a456277be0c9f497b63d602be35e Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 14 Dec 2022 14:14:44 +0000 Subject: [PATCH] call out to Octopus Imaging API from `database-bridge-lambda` in `createItem` mutation if the `type` is `imaging-request` (plus handle failure better in the client) --- cdk/lib/__snapshots__/stack.test.ts.snap | 86 +++++++++++++++++++ cdk/lib/stack.ts | 9 ++ client/src/app.tsx | 2 +- client/src/itemInputBox.tsx | 3 +- .../src/push-notifications/serviceWorker.ts | 6 +- client/src/sendMessageArea.tsx | 15 ++-- client/src/types/ItemWithParsedPayload.ts | 5 -- database-bridge-lambda/package.json | 1 + .../src/imagingRequestCallout.ts | 52 +++++++++++ database-bridge-lambda/src/sql/Item.ts | 25 ++++-- shared/environmentVariables.ts | 1 + shared/types/ItemWithParsedPayload.ts | 5 ++ yarn.lock | 1 + 13 files changed, 190 insertions(+), 21 deletions(-) delete mode 100644 client/src/types/ItemWithParsedPayload.ts create mode 100644 database-bridge-lambda/src/imagingRequestCallout.ts create mode 100644 shared/types/ItemWithParsedPayload.ts diff --git a/cdk/lib/__snapshots__/stack.test.ts.snap b/cdk/lib/__snapshots__/stack.test.ts.snap index 5e58d1dc..b1a740cf 100644 --- a/cdk/lib/__snapshots__/stack.test.ts.snap +++ b/cdk/lib/__snapshots__/stack.test.ts.snap @@ -4007,6 +4007,39 @@ $util.toJson($ctx.result)", "Endpoint", ], }, + "OCTOPUS_API_LAMBDA_FUNCTION_NAME": Object { + "Fn::Select": Array [ + 6, + Object { + "Fn::Split": Array [ + ":", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:", + Object { + "Fn::ImportValue": "octopus-api-TEST-function-name", + }, + ], + ], + }, + ], + }, + ], + }, "STACK": "workflow", "STAGE": "TEST", }, @@ -4166,6 +4199,59 @@ $util.toJson($ctx.result)", ], }, }, + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:", + Object { + "Fn::ImportValue": "octopus-api-TEST-function-name", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":lambda:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":function:", + Object { + "Fn::ImportValue": "octopus-api-TEST-function-name", + }, + ":*", + ], + ], + }, + ], + }, ], "Version": "2012-10-17", }, diff --git a/cdk/lib/stack.ts b/cdk/lib/stack.ts index b53cb229..6bf7823b 100644 --- a/cdk/lib/stack.ts +++ b/cdk/lib/stack.ts @@ -277,6 +277,12 @@ export class PinBoardStack extends GuStack { `Allow ${databaseSecurityGroupName} to connect to the ${databaseProxy.dbProxyName}` ); + const octopusApiLambda = lambda.Function.fromFunctionName( + this, + "OctopusApiLambda", + Fn.importValue(`octopus-api-${this.stage}-function-name`) + ); + const pinboardDatabaseBridgeLambda = new lambda.Function( this, DATABASE_BRIDGE_LAMBDA_BASENAME, @@ -290,6 +296,8 @@ export class PinBoardStack extends GuStack { STACK: this.stack, APP, [ENVIRONMENT_VARIABLE_KEYS.databaseHostname]: databaseHostname, + [ENVIRONMENT_VARIABLE_KEYS.octopusApiLambdaFunctionName]: + octopusApiLambda.functionName, }, functionName: getDatabaseBridgeLambdaFunctionName(this.stage as Stage), code: lambda.Code.fromBucket( @@ -302,6 +310,7 @@ export class PinBoardStack extends GuStack { } ); databaseProxy.grantConnect(pinboardDatabaseBridgeLambda); + octopusApiLambda.grantInvoke(pinboardDatabaseBridgeLambda); const databaseJumpHostASGName = getDatabaseJumpHostAsgName( this.stage as Stage diff --git a/client/src/app.tsx b/client/src/app.tsx index 74278775..e4f7ef0c 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -22,7 +22,7 @@ import { gqlSetWebPushSubscriptionForUser, } from "../gql"; import { Item, MyUser, User } from "shared/graphql/graphql"; -import { ItemWithParsedPayload } from "./types/ItemWithParsedPayload"; +import { ItemWithParsedPayload } from "shared/types/ItemWithParsedPayload"; import { HiddenIFrameForServiceWorker } from "./pushNotificationPreferences"; import { GlobalStateProvider } from "./globalState"; import { Floaty } from "./floaty"; diff --git a/client/src/itemInputBox.tsx b/client/src/itemInputBox.tsx index ca8f005b..b367aa9c 100644 --- a/client/src/itemInputBox.tsx +++ b/client/src/itemInputBox.tsx @@ -300,8 +300,7 @@ export const ItemInputBox = ({ payloadToBeSent?.type === IMAGING_REQUEST_ITEM_TYPE ? message : message?.trim() || payloadToBeSent; - if (!isAsGridPayloadLoading && hasSomethingToSend - ) { + if (!isAsGridPayloadLoading && hasSomethingToSend) { sendItem(); } event.preventDefault(); diff --git a/client/src/push-notifications/serviceWorker.ts b/client/src/push-notifications/serviceWorker.ts index 7758f89f..062c000a 100644 --- a/client/src/push-notifications/serviceWorker.ts +++ b/client/src/push-notifications/serviceWorker.ts @@ -1,10 +1,10 @@ -import { ItemWithParsedPayload } from "../types/ItemWithParsedPayload"; +import { ItemWithParsedPayload } from "shared/types/ItemWithParsedPayload"; import { EXPAND_PINBOARD_QUERY_PARAM, OPEN_PINBOARD_QUERY_PARAM, PINBOARD_ITEM_ID_QUERY_PARAM, -} from "../../../shared/constants"; -import { extractNameFromEmail } from "../../../shared/util"; +} from "shared/constants"; +import { extractNameFromEmail } from "shared/util"; const toolsDomain = self.location.hostname.replace("pinboard.", ""); diff --git a/client/src/sendMessageArea.tsx b/client/src/sendMessageArea.tsx index 835aae47..1a41e150 100644 --- a/client/src/sendMessageArea.tsx +++ b/client/src/sendMessageArea.tsx @@ -77,24 +77,29 @@ export const SendMessageArea = ({ const [_sendItem, { loading: isItemSending }] = useMutation<{ createItem: Item; }>(gqlCreateItem, { - onCompleted: (sendMessageResult) => { + onCompleted: ({ createItem }) => { + if (!createItem) { + return onError( + new ApolloError({ errorMessage: "Item creation failed" }) + ); + } onSuccessfulSend( { - ...sendMessageResult.createItem, + ...createItem, pending: true, }, verifiedIndividualMentionEmails ); sendTelemetryEvent?.(PINBOARD_TELEMETRY_TYPE.MESSAGE_SENT, { - pinboardId: sendMessageResult.createItem.pinboardId, + pinboardId: createItem.pinboardId, messageType: payloadToBeSent?.type || "message-only", hasMentions: !!verifiedIndividualMentionEmails.length || !!verifiedGroupMentionShorthands.length, hasIndividualMentions: !!verifiedIndividualMentionEmails.length, hasGroupMentions: !!verifiedGroupMentionShorthands.length, - isClaimable: sendMessageResult.createItem.claimable, - isReply: !!sendMessageResult.createItem.relatedItemId, + isClaimable: createItem.claimable, + isReply: !!createItem.relatedItemId, ...(composerId ? { composerId } : {}), }); setMessage(""); diff --git a/client/src/types/ItemWithParsedPayload.ts b/client/src/types/ItemWithParsedPayload.ts deleted file mode 100644 index 8829feca..00000000 --- a/client/src/types/ItemWithParsedPayload.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Item } from "../../../shared/graphql/graphql"; - -export type ItemWithParsedPayload = Item & { - payload: Record | null | undefined; -}; diff --git a/database-bridge-lambda/package.json b/database-bridge-lambda/package.json index eb541164..09afde80 100644 --- a/database-bridge-lambda/package.json +++ b/database-bridge-lambda/package.json @@ -10,6 +10,7 @@ "watch": "ts-node-dev --respawn run.ts" }, "devDependencies": { + "@aws-sdk/client-lambda": "^3.299.0", "ts-node-dev": "^1.0.0" }, "dependencies": { diff --git a/database-bridge-lambda/src/imagingRequestCallout.ts b/database-bridge-lambda/src/imagingRequestCallout.ts new file mode 100644 index 00000000..363d2344 --- /dev/null +++ b/database-bridge-lambda/src/imagingRequestCallout.ts @@ -0,0 +1,52 @@ +import { getEnvironmentVariableOrThrow } from "shared/environmentVariables"; +import { ItemWithParsedPayload } from "shared/types/ItemWithParsedPayload"; +import { InvokeCommand, LambdaClient, LogType } from "@aws-sdk/client-lambda"; +import { standardAwsConfig } from "shared/awsIntegration"; + +const lambda = new LambdaClient(standardAwsConfig); +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +export const performImagingRequest = async (item: ItemWithParsedPayload) => { + const gridId = (item.payload?.embeddableUrl as string)?.split("/").pop(); + if (!gridId) { + throw new Error(`Couldn't extract grid ID from payload: ${item.payload}`); + } + const imagingRequestBody = { + workflowId: item.pinboardId, + pinboardItemId: item.id, + lastUser: item.userEmail, + notes: item.message, //TODO check for 256 max (probably limit in UI too) + requestType: item.payload?.requestType, // TODO tighten this up + gridId, + // composerId: TODO lookup somehow + // pubDate TODO scheduled launch vs some date field in workflow - what's worse wrong date or no date? + // section TODO lookup somehow + // story group name TODO (synced from InCopy most likely, if available) + }; + console.log("Performing imaging request", imagingRequestBody); + + const octopusLambdaFunctionName = getEnvironmentVariableOrThrow( + "octopusApiLambdaFunctionName" + ); + + const octopusResponse = await lambda.send( + new InvokeCommand({ + FunctionName: octopusLambdaFunctionName, + Payload: textEncoder.encode(JSON.stringify(imagingRequestBody)), + LogType: LogType.None, //TODO consider whether we tail the octopus logs as pinboard logs + }) + ); + + if (octopusResponse.FunctionError) { + console.error(octopusResponse.FunctionError); + throw Error(octopusResponse.FunctionError); + } else { + console.log( + "Imaging request complete", + JSON.parse(textDecoder.decode(octopusResponse.Payload)) + ); + + // FIXME return something from octopusResponse.Payload + } +}; diff --git a/database-bridge-lambda/src/sql/Item.ts b/database-bridge-lambda/src/sql/Item.ts index efe977a7..083588a2 100644 --- a/database-bridge-lambda/src/sql/Item.ts +++ b/database-bridge-lambda/src/sql/Item.ts @@ -3,9 +3,12 @@ import { EditItemInput, Item, PinboardIdWithClaimCounts, -} from "../../../shared/graphql/graphql"; -import { Sql } from "../../../shared/database/types"; -import { Range } from "../../../shared/types/grafanaType"; +} from "shared/graphql/graphql"; +import { Sql } from "shared/database/types"; +import { Range } from "shared/types/grafanaType"; +import { performImagingRequest } from "../imagingRequestCallout"; +import { IMAGING_REQUEST_ITEM_TYPE } from "shared/octopusImaging"; +import { ItemWithParsedPayload } from "shared/types/ItemWithParsedPayload"; const fragmentIndividualMentionsToMentionHandles = ( sql: Sql, @@ -59,10 +62,22 @@ export const createItem = async ( args: { input: CreateItemInput }, userEmail: string ) => - sql` + sql.begin(async (sql) => { + const insertResult = (await sql` INSERT INTO "Item" ${sql({ userEmail, ...args.input })} RETURNING ${fragmentItemFields(sql, userEmail)} - `.then((rows) => rows[0]); + `.then((rows) => rows[0])) as ItemWithParsedPayload; + if ( + insertResult.type === IMAGING_REQUEST_ITEM_TYPE && + insertResult.payload + ) { + // if this throws, the SQL transaction should be rolled back + await performImagingRequest(insertResult); + + //TODO return/store octopus ID + } + return insertResult; + }); export const editItem = async ( sql: Sql, diff --git a/shared/environmentVariables.ts b/shared/environmentVariables.ts index 36078e65..97510356 100644 --- a/shared/environmentVariables.ts +++ b/shared/environmentVariables.ts @@ -3,6 +3,7 @@ export const ENVIRONMENT_VARIABLE_KEYS = { graphqlEndpoint: "GRAPHQL_ENDPOINT", sentryDSN: "SENTRY_DSN", databaseHostname: "DATABASE_HOSTNAME", + octopusApiLambdaFunctionName: "OCTOPUS_API_LAMBDA_FUNCTION_NAME", }; export const getEnvironmentVariableOrThrow = ( diff --git a/shared/types/ItemWithParsedPayload.ts b/shared/types/ItemWithParsedPayload.ts new file mode 100644 index 00000000..7c39b1d0 --- /dev/null +++ b/shared/types/ItemWithParsedPayload.ts @@ -0,0 +1,5 @@ +import { Item } from "../graphql/graphql"; + +export type ItemWithParsedPayload = Omit & { + payload: Record | null | undefined; +}; diff --git a/yarn.lock b/yarn.lock index 95293702..410b65a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9189,6 +9189,7 @@ __metadata: version: 0.0.0-use.local resolution: "database-bridge-lambda@workspace:database-bridge-lambda" dependencies: + "@aws-sdk/client-lambda": "npm:^3.299.0" postgres: "npm:^3.2.4" ts-node-dev: "npm:^1.0.0" languageName: unknown