Skip to content
Closed
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 docs import from @ciscode/notification-kit/infra, but the package’s exports only exposes the root entry ("."). This subpath import will fail in Node/TS with modern resolution. Either update examples to import from @ciscode/notification-kit (recommended given current exports) or add an ./infra subpath export in package.json.

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)
);
```

### 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();
Comment on lines +157 to +159
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.

Same issue as above: @ciscode/notification-kit/infra is not an exported subpath per package.json exports, so this import will not work for consumers. Update to @ciscode/notification-kit or add the appropriate subpath export.

Copilot uses AI. Check for mistakes.

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 "not exported by default", but src/index.ts re-exports ./infra, so they are part of the package’s public surface. Please update this comment to reflect the actual export behavior (or adjust exports if the intent is to keep infra internal).

Suggested change
* These implementations are internal and not exported by default.
* They can be used when configuring the NestJS module.
* These implementations are primarily intended for internal wiring and NestJS module configuration.
* They are re-exported and available to consumers, but are not the preferred abstraction layer.

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;
Comment on lines +68 to +69
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 not provided. This makes the in-memory implementation behave differently from the Mongoose repository (which returns all results unless a limit is specified) and can silently truncate results. Consider only slicing when limit is explicitly set (and treat offset similarly), or default limit to results.length when undefined.

Suggested change
const offset = _criteria.offset || 0;
const limit = _criteria.limit || 10;
const offset = _criteria.offset ?? 0;
const limit = _criteria.limit ?? results.length - offset;

Copilot uses AI. Check for mistakes.

return results.slice(offset, offset + limit);
}

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";

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