Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6aadbc9
implemented decorators and providers
y-aithnini Feb 27, 2026
ada99be
Add notification and webhook controller tests
y-aithnini Mar 2, 2026
c486b99
feat: standardize package configuration and workflows (#2)
Zaiidmo Mar 3, 2026
571aecc
remove in-memory repository and update exports
y-aithnini Mar 3, 2026
73009e2
removed mongoose
y-aithnini Mar 3, 2026
4210d2c
Merge develop: resolve conflicts and update jest config for spec files
y-aithnini Mar 4, 2026
96a80f6
removed duplicate code for sonarqube
y-aithnini Mar 4, 2026
aac7189
docs: add comprehensive documentation for testing implementation
y-aithnini Mar 4, 2026
81e390e
style: fix prettier formatting issues
y-aithnini Mar 4, 2026
746f00e
Merge branch 'develop' of https://github.com/CISCODE-MA/NotificationK…
y-aithnini Mar 9, 2026
2b5c2f4
integrated whatsapp notification msg
y-aithnini Mar 10, 2026
0e0de92
updated configuration
y-aithnini Mar 11, 2026
804543a
fix: replace deprecated substr() with slice() in mock WhatsApp sender
y-aithnini Mar 11, 2026
5e2d970
fix: replace Math.random with crypto.randomUUID for secure ID generation
y-aithnini Mar 11, 2026
e2c1e12
Merge branch 'master' into feature/whatsapp
y-aithnini Mar 11, 2026
b27dd5d
fix: change ts-expect-error to ts-ignore in notification.schema.ts
y-aithnini Mar 11, 2026
d250e60
Merge branch 'develop' into feature/whatsapp
y-aithnini Mar 11, 2026
69ae2f6
fix: regenerate package-lock.json to sync with package.json
y-aithnini Mar 11, 2026
b6d7cb5
refactor(whatsapp): extract duplicate validation logic to shared utility
y-aithnini Mar 11, 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
903 changes: 903 additions & 0 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

55 changes: 15 additions & 40 deletions src/infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,62 +135,37 @@ const pushSender = new AwsSnsPushSender({

## 💾 Repositories

> **Note**: Repository implementations are provided by separate database packages.
> Install the appropriate package for your database:

### MongoDB

Install the MongoDB package:

```bash
npm install @ciscode/notification-kit-mongodb
```
### MongoDB with Mongoose

```typescript
import { MongooseNotificationRepository } from "@ciscode/notification-kit-mongodb";
import mongoose from "mongoose";
import { MongooseNotificationRepository } from "@ciscode/notification-kit/infra";

Comment on lines 141 to 143
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README examples import from @ciscode/notification-kit/infra, but package.json only exports the root entry ".". This subpath import will fail in Node (and often in TS). Either update docs to import from @ciscode/notification-kit or add an explicit ./infra subpath export.

Copilot uses AI. Check for mistakes.
const connection = await mongoose.createConnection("mongodb://localhost:27017/mydb");
const repository = new MongooseNotificationRepository(connection);
```

### PostgreSQL

Install the PostgreSQL package:

```bash
npm install @ciscode/notification-kit-postgres
const repository = new MongooseNotificationRepository(
connection,
"notifications", // collection name (optional)
);
Comment on lines 144 to +149
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await mongoose.createConnection(...) doesn’t actually await the connection opening (Mongoose returns a Connection, not a Promise). The example can lead to using the repository before the connection is ready. Consider updating the docs to use createConnection(...).asPromise() (or mongoose.connect() with mongoose.connection) so the sample is correct.

Copilot uses AI. Check for mistakes.
```

### Custom Repository
**Peer Dependency**: `mongoose`

Implement the `INotificationRepository` interface:
### In-Memory (Testing)

```typescript
import type { INotificationRepository, Notification } from "@ciscode/notification-kit";
import { InMemoryNotificationRepository } from "@ciscode/notification-kit/infra";

class MyCustomRepository implements INotificationRepository {
async create(data: Omit<Notification, "id" | "createdAt" | "updatedAt">): Promise<Notification> {
// Your implementation
}
const repository = new InMemoryNotificationRepository();

async findById(id: string): Promise<Notification | null> {
// Your implementation
}
// For testing - clear all data
repository.clear();

// ... implement other methods
}
// For testing - get all notifications
const all = repository.getAll();
```

### Schema Reference

The MongoDB schema is exported as a reference:

```typescript
import { notificationSchemaDefinition } from "@ciscode/notification-kit/infra";

// Use this as a reference for your own schema implementations
```
**No dependencies**

## 🛠️ Utility Providers

Expand Down
8 changes: 4 additions & 4 deletions src/infra/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
*
* This layer contains concrete implementations of the core interfaces.
* It includes:
* - Notification senders (email, SMS, push)
* - Repository schemas (reference implementations)
* - Notification senders (email, SMS, push, WhatsApp)
* - Repositories (MongoDB, in-memory)
* - Utility providers (ID generator, datetime, templates, events)
*
* NOTE: Repository implementations are provided by separate database packages.
* Install the appropriate package: @ciscode/notification-kit-mongodb, etc.
* These implementations are internal and not exported by default.
* They can be used when configuring the NestJS module.
Comment on lines +10 to +11
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says infra implementations are "internal and not exported by default", but src/index.ts currently re-exports ./infra, so these symbols are exported by default. Please align this documentation with the actual public API (or adjust exports if the intent is to keep infra private).

Suggested change
* These implementations are internal and not exported by default.
* They can be used when configuring the NestJS module.
* These implementations are part of the public infrastructure layer and are
* exported so they can be used when configuring the NestJS module or for
* advanced/custom integrations.

Copilot uses AI. Check for mistakes.
*/

// Senders
Expand Down
178 changes: 178 additions & 0 deletions src/infra/repositories/in-memory/in-memory.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type {
INotificationRepository,
Notification,
NotificationQueryCriteria,
} from "../../../core";

/**
* In-memory repository implementation for testing/simple cases
*/
export class InMemoryNotificationRepository implements INotificationRepository {
private notifications: Map<string, Notification> = new Map();
private idCounter = 1;

async create(
_notification: Omit<Notification, "id" | "createdAt" | "updatedAt">,
): Promise<Notification> {
const now = new Date().toISOString();
const id = `notif_${this.idCounter++}`;

const notification: Notification = {
id,
..._notification,
createdAt: now,
updatedAt: now,
};

this.notifications.set(id, notification);

return notification;
}

async findById(_id: string): Promise<Notification | null> {
return this.notifications.get(_id) || null;
}

async find(_criteria: NotificationQueryCriteria): Promise<Notification[]> {
let results = Array.from(this.notifications.values());

// Apply filters
if (_criteria.recipientId) {
results = results.filter((n) => n.recipient.id === _criteria.recipientId);
}

if (_criteria.channel) {
results = results.filter((n) => n.channel === _criteria.channel);
}

if (_criteria.status) {
results = results.filter((n) => n.status === _criteria.status);
}

if (_criteria.priority) {
results = results.filter((n) => n.priority === _criteria.priority);
}

if (_criteria.fromDate) {
results = results.filter((n) => n.createdAt >= _criteria.fromDate!);
}

if (_criteria.toDate) {
results = results.filter((n) => n.createdAt <= _criteria.toDate!);
}

// Sort by createdAt descending
results.sort((a, b) => (b.createdAt > a.createdAt ? 1 : -1));

// Apply pagination
const offset = _criteria.offset || 0;
const limit = _criteria.limit || 10;

return results.slice(offset, offset + limit);
Comment on lines +68 to +71
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find() applies a default limit of 10 when _criteria.limit is undefined. The port contract treats limit as optional; the Mongoose implementation returns all results when no limit is provided. To keep behavior consistent across repositories, only apply slice when limit is explicitly set (and likewise only apply offset when provided).

Suggested change
const offset = _criteria.offset || 0;
const limit = _criteria.limit || 10;
return results.slice(offset, offset + limit);
const offset = _criteria.offset ?? 0;
if (typeof _criteria.limit === "number") {
return results.slice(offset, offset + _criteria.limit);
}
if (typeof _criteria.offset === "number") {
return results.slice(offset);
}
return results;

Copilot uses AI. Check for mistakes.
}

async update(_id: string, _updates: Partial<Notification>): Promise<Notification> {
const notification = this.notifications.get(_id);

if (!notification) {
throw new Error(`Notification with id ${_id} not found`);
}

const updated: Notification = {
...notification,
..._updates,
id: notification.id, // Preserve ID
createdAt: notification.createdAt, // Preserve createdAt
updatedAt: new Date().toISOString(),
};

this.notifications.set(_id, updated);

return updated;
}

async delete(_id: string): Promise<boolean> {
return this.notifications.delete(_id);
}

async count(_criteria: NotificationQueryCriteria): Promise<number> {
let results = Array.from(this.notifications.values());

// Apply filters
if (_criteria.recipientId) {
results = results.filter((n) => n.recipient.id === _criteria.recipientId);
}

if (_criteria.channel) {
results = results.filter((n) => n.channel === _criteria.channel);
}

if (_criteria.status) {
results = results.filter((n) => n.status === _criteria.status);
}

if (_criteria.priority) {
results = results.filter((n) => n.priority === _criteria.priority);
}

if (_criteria.fromDate) {
results = results.filter((n) => n.createdAt >= _criteria.fromDate!);
}

if (_criteria.toDate) {
results = results.filter((n) => n.createdAt <= _criteria.toDate!);
}

return results.length;
}

async findReadyToSend(_limit: number): Promise<Notification[]> {
const now = new Date().toISOString();
let results = Array.from(this.notifications.values());

// Find notifications ready to send
results = results.filter((n) => {
// Pending notifications that are scheduled and ready
if (n.status === "pending" && n.scheduledFor && n.scheduledFor <= now) {
return true;
}

// Queued notifications (ready to send immediately)
if (n.status === "queued") {
return true;
}

// Failed notifications that haven't exceeded retry count
if (n.status === "failed" && n.retryCount < n.maxRetries) {
return true;
}

return false;
});

// Sort by priority (high to low) then by createdAt (oldest first)
const priorityOrder: Record<string, number> = { urgent: 4, high: 3, normal: 2, low: 1 };
results.sort((a, b) => {
const priorityDiff = (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0);
if (priorityDiff !== 0) return priorityDiff;
return a.createdAt > b.createdAt ? 1 : -1;
});

return results.slice(0, _limit);
}

/**
* Clear all notifications (for testing)
*/
clear(): void {
this.notifications.clear();
this.idCounter = 1;
}

/**
* Get all notifications (for testing)
*/
getAll(): Notification[] {
return Array.from(this.notifications.values());
}
}
18 changes: 5 additions & 13 deletions src/infra/repositories/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
/**
* Repository schemas and types
*
* NOTE: Concrete repository implementations are provided by separate packages.
* Install the appropriate database package:
* - @ciscode/notification-kit-mongodb
* - @ciscode/notification-kit-postgres
* - etc.
*
* These schemas serve as reference for implementing your own repository.
*/

// MongoDB/Mongoose schema (reference)
// MongoDB/Mongoose repository
export * from "./mongoose/notification.schema";
export * from "./mongoose/mongoose.repository";
Comment on lines +1 to +3
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description template is still empty (Summary/Why/Notes/Checklist), but this PR introduces multiple user-facing changes (WhatsApp utils, new repository adapters, docs/instructions). Please fill in the description so reviewers understand scope and can verify the checklist items.

Copilot uses AI. Check for mistakes.

// In-memory repository
export * from "./in-memory/in-memory.repository";
Loading
Loading