Skip to content

Commit 7a3e01d

Browse files
authored
Merge pull request #45 from Sachintechjoomla/Issue#250717
Task#250717 Feat: Basic Setup In-App Notifications
2 parents f5856a2 + 0259e5a commit 7a3e01d

17 files changed

+1088
-10
lines changed

data-source.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import 'dotenv/config';
2+
import 'reflect-metadata';
3+
import { DataSource } from 'typeorm';
4+
5+
const AppDataSource = new DataSource({
6+
type: 'postgres',
7+
host: process.env.POSTGRES_HOST,
8+
port: Number(process.env.POSTGRES_PORT || 5432),
9+
username: process.env.POSTGRES_USERNAME,
10+
password: process.env.POSTGRES_PASSWORD,
11+
database: process.env.POSTGRES_DATABASE,
12+
synchronize: false,
13+
logging: false,
14+
entities: [
15+
'src/**/*.entity.ts',
16+
'src/**/entity/*.entity.ts',
17+
'src/modules/**/entity/*.entity.ts',
18+
],
19+
migrations: ['src/migrations/*.ts'],
20+
});
21+
22+
export default AppDataSource;
23+
24+

docs/in-app-notifications.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# In-App Notifications - API & Integration Guide
2+
3+
This document explains how to use the new in-app notifications in the Notification Service.
4+
5+
## Endpoints
6+
7+
- Create in-app notification (raw or template-based)
8+
- POST `notification/inApp`
9+
- List notifications with filters and pagination
10+
- GET `notification/inApp?userId={uuid}&status=unread|read|all&page=1&limit=20`
11+
- Count unread (reuse list with `limit=0`)
12+
- GET `notification/inApp?userId={uuid}&status=unread&limit=0`
13+
- Mark read (single or all)
14+
- PATCH `notification/inApp/mark-read`
15+
16+
All endpoints require `Authorization: Bearer <token>` header (Swagger auth key `access-token`).
17+
18+
---
19+
20+
## Create (Raw)
21+
22+
POST `notification/inApp`
23+
24+
Body:
25+
```json
26+
{
27+
"userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c",
28+
"title": "Task Completed",
29+
"message": "Your review task is completed.",
30+
"link": "https://app.example.com/tasks/T-9001",
31+
"metadata": { "taskId": "T-9001" },
32+
"tenant_code": "TENANT1",
33+
"org_code": "ORG1",
34+
"expiresAt": "2026-01-01T00:00:00.000Z"
35+
}
36+
```
37+
38+
Notes:
39+
- `title` is required only when neither `templateId` nor `key` is provided.
40+
- `message` (body) is stored at create time from template and replacements (if provided). List returns stored message directly.
41+
- Optional fields: `link`, `metadata`, `tenant_code`, `org_code`, `expiresAt`.
42+
43+
---
44+
45+
## Create (Template-based)
46+
47+
POST `notification/inApp`
48+
49+
Body:
50+
```json
51+
{
52+
"userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c",
53+
"templateId": "0d3b4b24-8a00-4f1d-9a1c-2f6f1c5f2b11",
54+
"replacements": {
55+
"userName": "Sachin",
56+
"{taskId}": "T-9001"
57+
},
58+
"metadata": { "priority": "high" }
59+
}
60+
```
61+
62+
Rules:
63+
- The template is taken from `NotificationActionTemplates` with `type='inApp'`.
64+
- Placeholders in the template subject/body/link are replaced by `replacements`.
65+
- Keys can be provided either with braces (`"{userName}"`) or without (`"userName"`).
66+
- If both `templateId` and raw `title/message` are provided, the template takes precedence.
67+
68+
---
69+
70+
## Create (Action-based, consistent with Email/SMS/Push)
71+
72+
POST `notification/inApp`
73+
74+
Body:
75+
```json
76+
{
77+
"userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c",
78+
"context": "USER",
79+
"key": "OnRegister",
80+
"replacements": {
81+
"userName": "Sachin"
82+
},
83+
"metadata": { "role": "superuser" }
84+
}
85+
```
86+
87+
Rules:
88+
- We resolve `NotificationActions` by `context` and `key`, then pick the `NotificationActionTemplates` with `type='inApp'`.
89+
- Placeholders are replaced using `replacements`.
90+
- We store `context`, `key`, and resolved fields; template is used transiently at create time.
91+
92+
---
93+
94+
## Create (Key-only)
95+
96+
POST `notification/inApp`
97+
98+
Body:
99+
```json
100+
{
101+
"userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c",
102+
"key": "OnRegister",
103+
"replacements": { "userName": "Sachin" }
104+
}
105+
```
106+
107+
Rules:
108+
- If `key` resolves to a single action, we use that. If multiple actions share the same key, provide `context` as well.
109+
- We fetch the in-app template for the resolved action and perform placeholder replacement.
110+
111+
---
112+
113+
## Create (Send-structure, bulk over recipients)
114+
115+
POST `notification/inApp`
116+
117+
Curl:
118+
```bash
119+
curl -X POST http://localhost:4000/notification/inApp \
120+
-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
121+
-d '{
122+
"context":"USER",
123+
"key":"OnRegister",
124+
"replacements":{"userName":"Sachin"},
125+
"inApp": { "receipients": ["<USER_UUID_1>", "<USER_UUID_2>"] }
126+
}'
127+
```
128+
129+
Behavior:
130+
- For each userId in `inApp.receipients`, the service resolves the action/template and creates an in-app record with stored title/message/link and metadata.
131+
- Response returns `{ inApp: { data: [{recipient, id}, ...] } }`.
132+
133+
## List
134+
135+
GET `notification/inApp?userId={uuid}&status=unread|read|all&offset=0&limit=20`
136+
137+
Examples:
138+
```
139+
GET notification/inApp?userId=c12f...&status=unread&offset=0&limit=20
140+
GET notification/inApp?userId=c12f...&status=read
141+
GET notification/inApp?userId=c12f... (defaults to status=all)
142+
```
143+
144+
Response:
145+
```json
146+
{
147+
"id": "api.send.notification",
148+
"responseCode": "OK",
149+
"result": {
150+
"data": [
151+
{
152+
"id": "7f7e6b10-5bdc-4f8b-bf4b-3e4f11a1b2c3",
153+
"userId": "c12f...",
154+
"title": "Task Completed",
155+
"message": "Your review task is completed.",
156+
"link": "https://app.example.com/tasks/T-9001",
157+
"metadata": { "taskId": "T-9001" },
158+
"isRead": false,
159+
"createdAt": "2025-12-05T10:30:00.000Z",
160+
"readAt": null,
161+
"expiresAt": null
162+
}
163+
],
164+
"count": 1,
165+
"offset": 0,
166+
"limit": 20
167+
}
168+
}
169+
```
170+
171+
---
172+
173+
## Count Unread
174+
175+
GET `notification/inApp?userId={uuid}&status=unread&limit=0`
176+
177+
Response:
178+
```json
179+
{
180+
"id": "api.send.notification",
181+
"responseCode": "OK",
182+
"result": { "count": 3 }
183+
}
184+
```
185+
186+
---
187+
188+
## Mark Read
189+
190+
PATCH `notification/inApp/mark-read`
191+
192+
- Mark single:
193+
```json
194+
{ "notificationId": "7f7e6b10-5bdc-4f8b-bf4b-3e4f11a1b2c3" }
195+
```
196+
197+
- Mark all for a user:
198+
```json
199+
{ "userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c", "markAll": true }
200+
```
201+
202+
Responses:
203+
- Success (single):
204+
```json
205+
{ "updated": 1, "message": "Notification marked as read" }
206+
```
207+
- Not found (single, already read or wrong id):
208+
```json
209+
{
210+
"id": "api.send.notification",
211+
"responseCode": "404",
212+
"params": { "status": "failed" },
213+
"result": {},
214+
"errmsg": "Notification not found or already read"
215+
}
216+
```
217+
- Success (all):
218+
```json
219+
{ "updated": 3, "message": "All notifications marked as read for user" }
220+
```
221+
222+
---
223+
224+
## Logging
225+
226+
All in-app operations write to `notificationLogs` for audit:
227+
- Create (raw or template): `type='inApp'`, `action='create'`, `subject=title`, `recipient=userId`
228+
- Mark read (single): `type='inApp'`, `action='mark-read-single'`, body includes notificationId
229+
- Mark read (all): `type='inApp'`, `action='mark-read-all'`, body includes affected count and userId
230+
231+
Use these logs for tracing, analytics, and support.
232+
233+
---
234+
235+
## Kafka Integration (optional)
236+
237+
If you publish to Kafka to create in-app notifications, use the unified service topic:
238+
- `notifications`
239+
240+
Message formats:
241+
242+
1) Raw create (channel discriminator)
243+
```json
244+
{
245+
"channel": "inApp",
246+
"userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c",
247+
"title": "Task Completed",
248+
"message": "Your review task is completed.",
249+
"link": "https://app.example.com/tasks/T-9001",
250+
"metadata": { "taskId": "T-9001" },
251+
"tenant_code": "TENANT1",
252+
"org_code": "ORG1",
253+
"expiresAt": "2026-01-01T00:00:00.000Z",
254+
"traceId": "d9f7a1a0-6e4f-4a44-a9d0-1234567890ab"
255+
}
256+
```
257+
258+
2) Key-based create (channel discriminator)
259+
```json
260+
{
261+
"channel": "inApp",
262+
"userId": "c12fa913-6fc1-4e90-bd44-2c75724f2b3c",
263+
"key": "OnRegister",
264+
"context": "USER",
265+
"replacements": {
266+
"userName": "Sachin",
267+
"{taskId}": "T-9001"
268+
},
269+
"metadata": { "priority": "high" },
270+
"traceId": "d9f7a1a0-6e4f-4a44-a9d0-1234567890ab"
271+
}
272+
```
273+
274+
Consumer handling guideline:
275+
- Validate `userId` (UUID).
276+
- Route by `channel` (e.g., `email`, `sms`, `push`, `inApp`) under the single `notifications` topic.
277+
- Prefer resolving by `key` (and optional `context`) to find `NotificationActions`, then fetch `NotificationActionTemplates` with `type='inApp'`. Render subject/body/link using `replacements`.
278+
- Fallbacks:
279+
- If only `templateId` exists, render using that (not stored).
280+
- Else require raw `title` and `message`.
281+
- Persist record into `notificationInApp`.
282+
- Optionally write an entry in `notificationLogs` with `type='inApp'`.
283+
- Idempotency suggestion: use `traceId` as a de-duplication key in your consumer if needed.
284+
285+
---
286+
287+
## Migrations
288+
289+
Run migrations (requires DB env vars present):
290+
```bash
291+
npm run migration:run
292+
```
293+
294+
Revert last migration:
295+
```bash
296+
npm run migration:revert
297+
```
298+
299+
Dev tip: to generate a new migration from current models
300+
```bash
301+
npm run migration:generate
302+
```
303+
304+

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
"test:watch": "jest --watch",
1919
"test:cov": "jest --coverage",
2020
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21-
"test:e2e": "jest --config ./test/jest-e2e.json"
21+
"test:e2e": "jest --config ./test/jest-e2e.json",
22+
"migration:create": "typeorm-ts-node-commonjs migration:create src/migrations/Migration",
23+
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d ./data-source.ts src/migrations/AutoMigration",
24+
"migration:run": "typeorm-ts-node-commonjs migration:run -d ./data-source.ts",
25+
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d ./data-source.ts"
2226
},
2327
"dependencies": {
2428
"@golevelup/nestjs-rabbitmq": "^5.3.0",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class CreateInAppNotification1764892800000 implements MigrationInterface {
4+
name = 'CreateInAppNotification1764892800000'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE IF NOT EXISTS public."notificationInApp" (
9+
id uuid PRIMARY KEY,
10+
user_id uuid NOT NULL,
11+
context varchar(255),
12+
action_key varchar(255),
13+
tenant_code varchar(255),
14+
org_code varchar(255),
15+
title varchar(255) NOT NULL,
16+
message text,
17+
link varchar(500),
18+
metadata jsonb,
19+
is_read boolean NOT NULL DEFAULT false,
20+
created_at timestamptz NOT NULL DEFAULT now(),
21+
read_at timestamptz,
22+
expires_at timestamptz,
23+
-- no source, template_params, action_id
24+
);
25+
`);
26+
27+
await queryRunner.query(`
28+
CREATE INDEX IF NOT EXISTS idx_inapp_user_created ON public."notificationInApp" (user_id, created_at DESC);
29+
`);
30+
await queryRunner.query(`
31+
CREATE INDEX IF NOT EXISTS idx_inapp_user_isread_created ON public."notificationInApp" (user_id, is_read, created_at DESC);
32+
`);
33+
await queryRunner.query(`
34+
CREATE INDEX IF NOT EXISTS idx_inapp_expires_at ON public."notificationInApp" (expires_at);
35+
`);
36+
}
37+
38+
public async down(queryRunner: QueryRunner): Promise<void> {
39+
await queryRunner.query(`DROP INDEX IF EXISTS idx_inapp_expires_at;`);
40+
await queryRunner.query(`DROP INDEX IF EXISTS idx_inapp_user_isread_created;`);
41+
await queryRunner.query(`DROP INDEX IF EXISTS idx_inapp_user_created;`);
42+
await queryRunner.query(`DROP TABLE IF EXISTS public."notificationInApp";`);
43+
}
44+
}
45+
46+

0 commit comments

Comments
 (0)