Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dbe76fb
Initial commit
thiessenp-cds Dec 9, 2025
7787ecc
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 9, 2025
8e08762
Move notification to core
thiessenp-cds Dec 9, 2025
c0d2a78
Add missing dependencies
thiessenp-cds Dec 10, 2025
f91b340
Update comment
thiessenp-cds Dec 10, 2025
60f8689
Remove sqs queue url from connectors
thiessenp-cds Dec 10, 2025
854d837
Add more comments
thiessenp-cds Dec 10, 2025
9134232
Undo previous change
thiessenp-cds Dec 10, 2025
d1ef95c
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 11, 2025
c51419a
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 11, 2025
e8bfe99
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 15, 2025
157ded2
Move notification and utils to connectors
thiessenp-cds Dec 15, 2025
c7e8586
Undo previous package change
thiessenp-cds Dec 15, 2025
d980a0d
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 15, 2025
24f149b
Updatee error logging
thiessenp-cds Dec 15, 2025
9ab3837
Update logging typo
thiessenp-cds Dec 15, 2025
ab7a4ae
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 16, 2025
0ea5e83
Update notification utils to add cause to re-thrown errors
thiessenp-cds Dec 16, 2025
c00ddb3
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 17, 2025
249f725
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 17, 2025
08fe30e
Fix typo
thiessenp-cds Dec 17, 2025
b435a3f
Updates from PR review
thiessenp-cds Dec 17, 2025
f0376f0
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Dec 17, 2025
92b2ec1
Merge branch 'main' into feat/notification-package-update
thiessenp-cds Jan 5, 2026
594c420
Remove some no longer needed exports
thiessenp-cds Jan 5, 2026
aa368c0
Remove export from barrel file
thiessenp-cds Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/connectors/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.2.14] - 2025-12-15

- Add notification helpers
- Add utils for AWS

## [2.2.13] - 2025-10-27

- Update AWS SDK package from version `3.917.0`
Expand Down
2 changes: 1 addition & 1 deletion packages/connectors/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gcforms/connectors",
"version": "2.2.13",
"version": "2.2.14",
"author": "Canadian Digital Service",
"license": "MIT",
"publishConfig": {
Expand Down
2 changes: 1 addition & 1 deletion packages/connectors/src/gc-notify-connector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Agent } from "https";
import { getAwsSecret } from "./getAwsSecret";
import { getAwsSecret } from "./utils";
import axios, { AxiosError } from "axios";

const API_URL: string = "https://api.notification.canada.ca";
Expand Down
7 changes: 0 additions & 7 deletions packages/connectors/src/getAwsSecret.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/connectors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { GCNotifyConnector, type Personalisation } from "./gc-notify-connector";
export { PostgresConnector } from "./postgres-connector";
export { notification } from "./notification";
135 changes: 135 additions & 0 deletions packages/connectors/src/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
import { ErrorWithCause } from "./types/errors";
import { getAwsSQSQueueURL } from "./utils";

const DYNAMODB_NOTIFICATION_TABLE_NAME = "Notification";

const globalConfig = {
region: process.env.AWS_REGION ?? "ca-central-1",
};

const dynamoDBDocumentClient = DynamoDBDocumentClient.from(
new DynamoDBClient({
...globalConfig,
// SDK retries use exponential backoff with jitter by default
maxAttempts: 15,
})
);

const sqsClient = new SQSClient({
...globalConfig,
});

/**
* Creates a notification record in DynamoDB and enqueues it for immediate sending.
*
* @param emails - Array of email addresses to send the notification to
* @param subject - Email subject line
* @param body - Email body content
*/
const sendImmediate = async ({
emails,
subject,
body,
}: {
notificationId?: string;
emails: string[];
subject: string;
body: string;
}): Promise<void> => {
const notificationId = randomUUID();
try {
await _createRecord({ notificationId, emails, subject, body });
await enqueueDeferred(notificationId);
} catch (error) {
throw new ErrorWithCause(`Error creating immediate notification id ${notificationId}`, {
cause: error,
});
}
};

/**
* Creates a notification record in DynamoDB for deferred sending. Once the related
* process is completed it can enqueue the notification for sending by calling
* enqueueDeferredNotification with the related notificationId.
*
* @param notificationId - Unique identifier for the notification to enqueue and
* used by the notification lambda to look up the record in DynamoDB.
*/
const sendDeferred = async ({
notificationId,
emails,
subject,
body,
}: {
notificationId: string;
emails: string[];
subject: string;
body: string;
}): Promise<void> => {
try {
await _createRecord({ notificationId, emails, subject, body });
} catch (error) {
throw new ErrorWithCause(`Error creating deferred notification id ${notificationId}`, {
cause: error,
});
}
};

const _createRecord = async ({
notificationId,
emails,
subject,
body,
}: {
notificationId: string;
emails: string[];
subject: string;
body: string;
}): Promise<void> => {
try {
const ttl = Math.floor(Date.now() / 1000) + 86400; // 24 hours from now
const command = new PutCommand({
TableName: DYNAMODB_NOTIFICATION_TABLE_NAME,
Item: {
NotificationID: notificationId,
Emails: emails,
Subject: subject,
Body: body,
TTL: ttl,
},
});
await dynamoDBDocumentClient.send(command);
} catch (error) {
throw new ErrorWithCause(`Could not create record`, { cause: error });
}
};

const enqueueDeferred = async (notificationId: string): Promise<void> => {
try {
const queueUrl = await getAwsSQSQueueURL("NOTIFICATION_QUEUE_URL", "notification_queue");
if (!queueUrl) {
throw new Error("Notification Queue not connected");
}

const command = new SendMessageCommand({
MessageBody: JSON.stringify({ notificationId }),
QueueUrl: queueUrl,
});
const sendMessageCommandOutput = await sqsClient.send(command);
if (!sendMessageCommandOutput.MessageId) {
throw new Error("Received null SQS message identifier");
}
} catch (error) {
throw new ErrorWithCause(`Could not enqueue`, { cause: error });
}
};

export const notification = {
sendImmediate,
sendDeferred,
enqueueDeferred,
};
2 changes: 1 addition & 1 deletion packages/connectors/src/postgres-connector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAwsSecret } from "./getAwsSecret";
import { getAwsSecret } from "./utils";
import postgres, { Sql } from "postgres";

export class PostgresConnector {
Expand Down
17 changes: 17 additions & 0 deletions packages/connectors/src/types/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Custom Error class that supports adding a "cause" for re-throwing errors.
*/
export class ErrorWithCause extends Error {
cause?: unknown;

constructor(message: string, options?: { cause?: unknown }) {
super(message);
this.name = "ErrorWithCause";
this.cause = options?.cause;

// Maintains proper stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ErrorWithCause);
}
}
}
32 changes: 32 additions & 0 deletions packages/connectors/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SQSClient, GetQueueUrlCommand } from "@aws-sdk/client-sqs";
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

const globalConfig = {
region: process.env.AWS_REGION ?? "ca-central-1",
};

const sqsClient = new SQSClient({
...globalConfig,
});

export function getAwsSecret(secretIdentifier: string): Promise<string | undefined> {
return new SecretsManagerClient()
.send(new GetSecretValueCommand({ SecretId: secretIdentifier }))
.then((commandOutput) => commandOutput.SecretString);
}

export const getAwsSQSQueueURL = async (
urlEnvName: string,
urlQueueName: string
): Promise<string | null> => {
if (process.env[urlEnvName]) {
return process.env[urlEnvName];
}

const data = await sqsClient.send(
new GetQueueUrlCommand({
QueueName: urlQueueName,
})
);
return data.QueueUrl ?? null;
};
Loading