Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an infrastructure layer with concrete implementations of the core notification ports (senders, repositories, and utility providers), plus supporting documentation and config updates.
Changes:
- Add multiple infra senders (email/SMS/push) with lazy-loaded third-party providers.
- Add infra repositories (Mongoose + in-memory) and utility providers (ID, datetime, templates, events).
- Update TypeScript/lib config and package metadata/docs to support the new infra layer.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Adds DOM lib typings (needed for fetch-based infra). |
| package.json | Adds peerDependenciesMeta for optional infra provider dependencies. |
| src/infra/index.ts | New infra barrel entrypoint. |
| src/infra/README.md | Extensive infra usage docs and examples. |
| src/infra/senders/index.ts | Sender barrel exports. |
| src/infra/senders/email/nodemailer.sender.ts | Nodemailer email sender implementation. |
| src/infra/senders/sms/twilio.sender.ts | Twilio SMS sender implementation. |
| src/infra/senders/sms/aws-sns.sender.ts | AWS SNS SMS sender implementation. |
| src/infra/senders/sms/vonage.sender.ts | Vonage SMS sender implementation. |
| src/infra/senders/push/firebase.sender.ts | FCM push sender implementation. |
| src/infra/senders/push/onesignal.sender.ts | OneSignal push sender implementation using fetch. |
| src/infra/senders/push/aws-sns-push.sender.ts | AWS SNS push sender implementation. |
| src/infra/repositories/index.ts | Repository barrel exports (mongoose + in-memory). |
| src/infra/repositories/mongoose/notification.schema.ts | Mongoose schema definition for notifications. |
| src/infra/repositories/mongoose/mongoose.repository.ts | Mongoose-backed notification repository. |
| src/infra/repositories/in-memory/in-memory.repository.ts | In-memory repository implementation. |
| src/infra/providers/index.ts | Provider barrel exports. |
| src/infra/providers/datetime.provider.ts | Date/time provider implementation. |
| src/infra/providers/id-generator.provider.ts | ID generator implementations (UUID/ObjectId/NanoId). |
| src/infra/providers/template.provider.ts | Template engines (Handlebars + simple replacement). |
| src/infra/providers/event-emitter.provider.ts | Event emitters (in-memory + console). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (this.handlebars) { | ||
| return this.handlebars; | ||
| } | ||
|
|
There was a problem hiding this comment.
import("handlebars") is used without the @ts-expect-error pattern used elsewhere for optional peer deps. If handlebars isn’t installed in the build environment, TypeScript will fail module resolution. To keep this adapter optional and consistent with the other infra adapters, either add the suppression here or add handlebars (and typings) as a dev dependency while listing it as an optional peer dependency for consumers.
| // @ts-expect-error - optional peer dependency 'handlebars' may not be installed |
| ```typescript | ||
| import { NodemailerSender } from "@ciscode/notification-kit/infra"; | ||
|
|
||
| const emailSender = new NodemailerSender({ | ||
| host: "smtp.gmail.com", |
There was a problem hiding this comment.
The README examples import adapters from @ciscode/notification-kit/infra, but the package’s exports map currently only exposes . (and tsup only builds src/index.ts). As-is, this subpath won’t resolve for consumers. Either update the package build/exports to publish an ./infra entrypoint, or adjust the docs to match the actual public API surface.
| this.model = this.connection.model<NotificationDocument>( | ||
| "Notification", | ||
| schema, | ||
| this.collectionName, | ||
| ); |
There was a problem hiding this comment.
connection.model("Notification", schema, this.collectionName) uses a fixed model name. If multiple MongooseNotificationRepository instances share the same connection but use different collectionName values, Mongoose will reuse the first compiled model (or throw an overwrite error), leading to queries hitting the wrong collection. Consider incorporating collectionName into the model name (or calling connection.model(name) without a schema when it already exists).
| if (!_recipient.email) { | ||
| return { | ||
| success: false, | ||
| notificationId: "", |
There was a problem hiding this comment.
In the missing-email branch, notificationId is returned as an empty string. Even if the sender can’t know the notification entity id, returning "" makes it harder to correlate failures (and differs from the other senders). At minimum this should be consistent (e.g., return _recipient.id like other branches), and ideally the notification id should be passed in or set by the core service.
| notificationId: "", | |
| notificationId: _recipient.id, |
| platformApplicationArn: string; | ||
| } |
There was a problem hiding this comment.
AwsSnsPushConfig includes platformApplicationArn, but it’s never used. This is misleading for consumers and may indicate missing functionality (e.g., creating endpoints or scoping publish). Either remove it from the config or incorporate it into the implementation (and update the README example accordingly).
| platformApplicationArn: string; | |
| } | |
| } | |
| } |
| return { | ||
| success: true, | ||
| notificationId: _recipient.id, | ||
| providerMessageId: message["message-id"], |
There was a problem hiding this comment.
notificationId is returned as _recipient.id, which doesn’t align with the core NotificationResult contract (notification entity id). Since this sender doesn’t receive the notification id, the core service should set it (or the sender port should accept the notification id / Notification).
| // Helper to get Schema type at runtime (for Mongoose schema definitions) | ||
| const getSchemaTypes = () => { | ||
| try { | ||
| // @ts-expect-error - mongoose is an optional peer dependency | ||
| const mongoose = require("mongoose"); | ||
| return mongoose.Schema.Types; | ||
| } catch { | ||
| return { Mixed: {} }; | ||
| } |
There was a problem hiding this comment.
getSchemaTypes() uses require("mongoose"). In the ESM build (dist/index.mjs), require is not available, so this will always fall into the catch and return a dummy Mixed: {} even when mongoose is installed. That will make the schema definition invalid at runtime. Suggest using createRequire(import.meta.url) in ESM, or better: avoid this helper entirely and obtain Schema.Types from the actual mongoose instance already available via the connection when building the schema.
| // MongoDB/Mongoose repository | ||
| export * from "./mongoose/notification.schema"; | ||
| export * from "./mongoose/mongoose.repository"; | ||
|
|
||
| // In-memory repository | ||
| export * from "./in-memory/in-memory.repository"; |
There was a problem hiding this comment.
Exporting the mongoose-backed repository from this barrel means importing @ciscode/notification-kit/infra will pull in types that reference mongoose, forcing consumers to install it even if they only want in-memory adapters. To keep mongoose truly optional, consider splitting exports (e.g., infra/repositories/in-memory vs infra/repositories/mongoose) or using conditional exports so the base infra entrypoint doesn’t reference mongoose types.
| // MongoDB/Mongoose repository | |
| export * from "./mongoose/notification.schema"; | |
| export * from "./mongoose/mongoose.repository"; | |
| // In-memory repository | |
| export * from "./in-memory/in-memory.repository"; | |
| // In-memory repository | |
| export * from "./in-memory/in-memory.repository"; | |
| // NOTE: | |
| // Mongoose-backed repositories and schemas are intentionally *not* re-exported | |
| // from this barrel to keep `mongoose` optional for consumers that only use | |
| // in-memory adapters. Import Mongoose-specific modules from their dedicated | |
| // paths (e.g. "./mongoose/notification.schema" or "./mongoose/mongoose.repository"). |
|
|
||
| /** | ||
| * ID generator using UUID v4 | ||
| */ | ||
| export class UuidGenerator implements IIdGenerator { | ||
| generate(): string { | ||
| // Simple UUID v4 implementation | ||
| return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { | ||
| const r = (Math.random() * 16) | 0; | ||
| const v = c === "x" ? r : (r & 0x3) | 0x8; | ||
| return v.toString(16); | ||
| }); |
There was a problem hiding this comment.
UuidGenerator claims to generate UUID v4, but it relies on Math.random(), which is not cryptographically secure and can increase collision risk under load. Since the package targets Node >=20, consider using crypto.randomUUID() (or crypto.getRandomValues) for a standards-compliant UUID v4 implementation.
| /** | |
| * ID generator using UUID v4 | |
| */ | |
| export class UuidGenerator implements IIdGenerator { | |
| generate(): string { | |
| // Simple UUID v4 implementation | |
| return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { | |
| const r = (Math.random() * 16) | 0; | |
| const v = c === "x" ? r : (r & 0x3) | 0x8; | |
| return v.toString(16); | |
| }); | |
| import { randomUUID } from "crypto"; | |
| /** | |
| * ID generator using UUID v4 | |
| */ | |
| export class UuidGenerator implements IIdGenerator { | |
| generate(): string { | |
| // Use Node's built-in cryptographically secure UUID v4 generator | |
| return randomUUID(); |
| return { | ||
| success: true, | ||
| notificationId: _recipient.id, | ||
| providerMessageId: message.sid, | ||
| metadata: { | ||
| status: message.status, |
There was a problem hiding this comment.
All the returned NotificationResult.notificationId values are set to _recipient.id, but NotificationResult is defined as the notification ID (see core NotificationResult). Since send() only receives the recipient/content, these senders can’t know the notification entity id, so callers will get the wrong id on success. Suggestion: change the sender port to accept the notification id (or full Notification), or have NotificationService overwrite result.notificationId with notification.id before returning/storing it.
|


Summary
Why
Checklist
npm run lintpassesnpm run typecheckpassesnpm testpassesnpm run buildpassesnpx changeset) if this affects consumersNotes