Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
- Return the full HTTP response from the REST notification handler.
Note: With outbox enabled (default), the application's `await notify()` resolves when
the message is queued; the return value is only available when `outbox: false`.
- New default `auto` for `cds.env.requires.notifications.authenticationIdentifier`. Each recipient is inspected: UUID values are published with `GlobalUserId`, everything else with `RecipientId`, with a warning when a value is neither a UUID nor an email. The previous values `UserUUID` and `RecipientId` are still supported for an explicit choice.
In practice this means the plugin "just works" without configuration: applications can pass emails, UUIDs, or a mix of both, and the correct recipient key is chosen per value — no upfront configuration about Work Zone's authentication identifier required.

## Version 0.3.0

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@ To make notification types unique to the application, prefix is added to the typ

### Authentication Identifier

Depending on your Work Zone Notifications configuration, set `cds.env.requires.notifications.authenticationIdentifier` to `UserUUID` if the authenciation identifier in Work Zone is set to `User ID`. Notifications are then published with Recipient Key `GlobalUserId` instead of `RecipientId`. If not set, `RecipientId` is used. Note, that in order for E-Mail Notifications to be sent for notifications published with a User ID, a destination to the IDS needs to be configured for the lookup of the corresponding email address.
`cds.env.requires.notifications.authenticationIdentifier` controls which recipient key the plugin uses when publishing notifications. Supported values:

- `auto` (default): the recipient key is chosen per recipient. Values matching the UUID format are published with `GlobalUserId`, everything else with `RecipientId`. If a value is neither a UUID nor an email a warning is logged. This allows mixing UUIDs and emails in the same `recipients` array without additional configuration.
- `UserUUID`: always publish with `GlobalUserId`. Use this when the authentication identifier in Work Zone is set to `User ID`.
- `RecipientId`: always publish with `RecipientId`. Use this when you want to enforce email recipients.

Note, that in order for E-Mail Notifications to be sent for notifications published with a User ID, a destination to the IDS needs to be configured for the lookup of the corresponding email address.

For the Work Zone Authentication Identifier configuration details refer to: [Work Zone Subaccount Settings](https://help.sap.com/docs/build-work-zone-standard-edition/sap-build-work-zone-standard-edition/subaccount-settings)

Expand Down
21 changes: 13 additions & 8 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const cds = require("@sap/cds");
const LOG = cds.log('notifications');
const { getDestination } = require("@sap-cloud-sdk/connectivity");
const PRIORITIES = ["LOW", "NEUTRAL", "MEDIUM", "HIGH"];
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const messages = {
TYPES_FILE_NOT_EXISTS: "Notification Types file path is incorrect.",
Expand Down Expand Up @@ -116,13 +118,16 @@ async function getNotificationDestination() {
return notificationDestination;
}

function getRecipientKey() {
const authenticationIdentifier = cds.env.requires.notifications?.authenticationIdentifier;
var recipientKey = 'RecipientId'; // Email
if (authenticationIdentifier === "UserUUID") {
recipientKey = 'GlobalUserId';
function getRecipientKey(recipient) {
const authenticationIdentifier = cds.env.requires.notifications?.authenticationIdentifier ?? 'auto';
if (authenticationIdentifier === 'UserUUID') return 'GlobalUserId';
if (authenticationIdentifier === 'RecipientId') return 'RecipientId';
// 'auto' (and any unrecognized value): detect format per recipient
if (typeof recipient === 'string' && UUID_REGEX.test(recipient)) return 'GlobalUserId';
if (typeof recipient === 'string' && !EMAIL_REGEX.test(recipient)) {
LOG._warn && LOG.warn(`Recipient '${recipient}' is neither a UUID nor an email format. Falling back to RecipientId.`);
}
return recipientKey;
return 'RecipientId';
}

let prefix // be filled in below...
Expand Down Expand Up @@ -170,7 +175,7 @@ function buildDefaultNotification(
NotificationTypeVersion: "1",
Priority: priority,
Properties: properties,
Recipients: recipients.map((recipient) => ({ [getRecipientKey()]: recipient }))
Recipients: recipients.map((recipient) => ({ [getRecipientKey(recipient)]: recipient }))
};
}

Expand All @@ -179,7 +184,7 @@ function buildCustomNotification(_) {

// Properties with simple API variants
NotificationTypeKey: getNotificationTypesKeyWithPrefix(_.NotificationTypeKey || _.type),
Recipients: _.Recipients || _.recipients?.map(id => ({ [getRecipientKey()]: id })),
Recipients: _.Recipients || _.recipients?.map(id => ({ [getRecipientKey(id)]: id })),
Priority: _.Priority || _.priority || "NEUTRAL",
Properties: _.Properties || Object.entries(_.data).map(([k,v]) => ({
Key:k, Value:v, Language: "en", Type: typeof v, // IsSensitive: false
Expand Down
81 changes: 80 additions & 1 deletion tests/unit/lib/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,9 @@ describe("Test utils", () => {
});

describe("Configuration", () => {
const log = cds.test.log();
afterEach(() => { delete cds.env.requires.notifications?.authenticationIdentifier; });

it("Use GlobalUserId as the recipient key when authenticationIdentifier is set to UserUUID", () => {
cds.env.requires.notifications ??= {};
cds.env.requires.notifications.authenticationIdentifier = "UserUUID";
Expand All @@ -450,10 +453,86 @@ describe("Test utils", () => {
title: "Test Title"
});

delete cds.env.requires.notifications.authenticationIdentifier;
expect(result.Recipients[0]).toMatchObject({ GlobalUserId: "user-uuid-123" });
});

it("Auto mode picks GlobalUserId for UUID recipients", () => {
cds.env.requires.notifications ??= {};
cds.env.requires.notifications.authenticationIdentifier = "auto";

const result = buildNotification({
recipients: ["550e8400-e29b-41d4-a716-446655440000"],
title: "Test Title"
});

expect(result.Recipients[0]).toMatchObject({ GlobalUserId: "550e8400-e29b-41d4-a716-446655440000" });
});

it("Auto mode picks RecipientId for email recipients", () => {
cds.env.requires.notifications ??= {};
cds.env.requires.notifications.authenticationIdentifier = "auto";

const result = buildNotification({
recipients: ["test.mail@mail.com"],
title: "Test Title"
});

expect(result.Recipients[0]).toMatchObject({ RecipientId: "test.mail@mail.com" });
});

it("Auto mode supports mixed UUID and email recipients in one notification", () => {
cds.env.requires.notifications ??= {};
cds.env.requires.notifications.authenticationIdentifier = "auto";

const result = buildNotification({
recipients: ["550e8400-e29b-41d4-a716-446655440000", "test.mail@mail.com"],
title: "Test Title"
});

expect(result.Recipients).toEqual([
{ GlobalUserId: "550e8400-e29b-41d4-a716-446655440000" },
{ RecipientId: "test.mail@mail.com" }
]);
});

it("Auto mode warns and falls back to RecipientId when value is neither UUID nor email", () => {
cds.env.requires.notifications ??= {};
cds.env.requires.notifications.authenticationIdentifier = "auto";
log.clear();

const result = buildNotification({
recipients: ["not-a-uuid-or-email"],
title: "Test Title"
});

expect(result.Recipients[0]).toMatchObject({ RecipientId: "not-a-uuid-or-email" });
expect(log.output).toContain("neither a UUID nor an email");
});

it("Auto is the default when authenticationIdentifier is not configured", () => {
cds.env.requires.notifications ??= {};
delete cds.env.requires.notifications.authenticationIdentifier;

const result = buildNotification({
recipients: ["550e8400-e29b-41d4-a716-446655440000"],
title: "Test Title"
});

expect(result.Recipients[0]).toMatchObject({ GlobalUserId: "550e8400-e29b-41d4-a716-446655440000" });
});

it("Explicit RecipientId mode never resolves to GlobalUserId even for UUID values", () => {
cds.env.requires.notifications ??= {};
cds.env.requires.notifications.authenticationIdentifier = "RecipientId";

const result = buildNotification({
recipients: ["550e8400-e29b-41d4-a716-446655440000"],
title: "Test Title"
});

expect(result.Recipients[0]).toMatchObject({ RecipientId: "550e8400-e29b-41d4-a716-446655440000" });
});

it("Fall back to basename of cds.root as prefix when package.json cannot be read", () => {
let result;
jest.isolateModules(() => {
Expand Down
Loading