@yaghmori/notification-service 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/contracts.json +45 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +48 -0
- package/dist/config.js.map +1 -0
- package/dist/generated.d.ts +93 -0
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +59 -0
- package/dist/generated.js.map +1 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +61 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/kafka.d.ts +32 -0
- package/dist/kafka.d.ts.map +1 -0
- package/dist/kafka.js +41 -0
- package/dist/kafka.js.map +1 -0
- package/dist/nest.d.ts +7 -0
- package/dist/nest.d.ts.map +1 -0
- package/dist/nest.js +15 -0
- package/dist/nest.js.map +1 -0
- package/dist/schemas.d.ts +118 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +32 -0
- package/dist/schemas.js.map +1 -0
- package/docs/BEST_PRACTICES.md +8 -0
- package/docs/NUGET_TRUSTED_PUBLISHING.md +20 -0
- package/docs/PROTOCOL_GUIDE.md +208 -0
- package/docs/USAGE.md +153 -0
- package/package.json +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nest.d.ts","sourceRoot":"","sources":["../src/nest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAGpE,MAAM,MAAM,4BAA4B,GAAG,eAAe,GAAG;IAC3D,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,qBAAqB,CAOvB"}
|
package/dist/nest.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.notificationTcpClient = notificationTcpClient;
|
|
4
|
+
const microservices_1 = require("@nestjs/microservices");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const generated_1 = require("./generated");
|
|
7
|
+
function notificationTcpClient(options = {}) {
|
|
8
|
+
const { host, port } = (0, config_1.resolveTcpEndpoint)(options);
|
|
9
|
+
return {
|
|
10
|
+
name: options.name ?? generated_1.INJECTION_TOKEN,
|
|
11
|
+
transport: microservices_1.Transport.TCP,
|
|
12
|
+
options: { host, port },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=nest.js.map
|
package/dist/nest.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nest.js","sourceRoot":"","sources":["../src/nest.ts"],"names":[],"mappings":";;AASA,sDASC;AAjBD,yDAAkD;AAClD,qCAAoE;AACpE,2CAA8C;AAM9C,SAAgB,qBAAqB,CACnC,UAAwC,EAAE;IAE1C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAA,2BAAkB,EAAC,OAAO,CAAC,CAAC;IACnD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,2BAAe;QACrC,SAAS,EAAE,yBAAS,CAAC,GAAG;QACxB,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;KACxB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Payload for Kafka `notification.send.requested` / TCP create. */
|
|
3
|
+
export declare const sendNotificationRequestSchema: z.ZodObject<{
|
|
4
|
+
userId: z.ZodString;
|
|
5
|
+
eventKey: z.ZodString;
|
|
6
|
+
title: z.ZodOptional<z.ZodString>;
|
|
7
|
+
body: z.ZodOptional<z.ZodString>;
|
|
8
|
+
channels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
9
|
+
emailTemplate: z.ZodOptional<z.ZodString>;
|
|
10
|
+
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
11
|
+
actionUrl: z.ZodOptional<z.ZodString>;
|
|
12
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
13
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
userId: string;
|
|
16
|
+
eventKey: string;
|
|
17
|
+
title?: string | undefined;
|
|
18
|
+
body?: string | undefined;
|
|
19
|
+
channels?: string[] | undefined;
|
|
20
|
+
emailTemplate?: string | undefined;
|
|
21
|
+
data?: Record<string, unknown> | undefined;
|
|
22
|
+
actionUrl?: string | undefined;
|
|
23
|
+
tenantId?: string | undefined;
|
|
24
|
+
metadata?: Record<string, unknown> | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
userId: string;
|
|
27
|
+
eventKey: string;
|
|
28
|
+
title?: string | undefined;
|
|
29
|
+
body?: string | undefined;
|
|
30
|
+
channels?: string[] | undefined;
|
|
31
|
+
emailTemplate?: string | undefined;
|
|
32
|
+
data?: Record<string, unknown> | undefined;
|
|
33
|
+
actionUrl?: string | undefined;
|
|
34
|
+
tenantId?: string | undefined;
|
|
35
|
+
metadata?: Record<string, unknown> | undefined;
|
|
36
|
+
}>;
|
|
37
|
+
export type SendNotificationRequest = z.infer<typeof sendNotificationRequestSchema>;
|
|
38
|
+
export declare const listNotificationsRequestSchema: z.ZodObject<{
|
|
39
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
40
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
unreadOnly: z.ZodOptional<z.ZodBoolean>;
|
|
43
|
+
}, "strip", z.ZodTypeAny, {
|
|
44
|
+
userId?: string | undefined;
|
|
45
|
+
limit?: number | undefined;
|
|
46
|
+
offset?: number | undefined;
|
|
47
|
+
unreadOnly?: boolean | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
userId?: string | undefined;
|
|
50
|
+
limit?: number | undefined;
|
|
51
|
+
offset?: number | undefined;
|
|
52
|
+
unreadOnly?: boolean | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
export type ListNotificationsRequest = z.infer<typeof listNotificationsRequestSchema>;
|
|
55
|
+
export declare const getNotificationsRequestSchema: z.ZodObject<{
|
|
56
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
57
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
unreadOnly: z.ZodOptional<z.ZodBoolean>;
|
|
60
|
+
}, "strip", z.ZodTypeAny, {
|
|
61
|
+
userId?: string | undefined;
|
|
62
|
+
limit?: number | undefined;
|
|
63
|
+
offset?: number | undefined;
|
|
64
|
+
unreadOnly?: boolean | undefined;
|
|
65
|
+
}, {
|
|
66
|
+
userId?: string | undefined;
|
|
67
|
+
limit?: number | undefined;
|
|
68
|
+
offset?: number | undefined;
|
|
69
|
+
unreadOnly?: boolean | undefined;
|
|
70
|
+
}>;
|
|
71
|
+
export type GetNotificationsRequest = ListNotificationsRequest;
|
|
72
|
+
export declare const markAsReadRequestSchema: z.ZodObject<{
|
|
73
|
+
id: z.ZodString;
|
|
74
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
75
|
+
}, "strip", z.ZodTypeAny, {
|
|
76
|
+
id: string;
|
|
77
|
+
userId?: string | undefined;
|
|
78
|
+
}, {
|
|
79
|
+
id: string;
|
|
80
|
+
userId?: string | undefined;
|
|
81
|
+
}>;
|
|
82
|
+
export type MarkAsReadRequest = z.infer<typeof markAsReadRequestSchema>;
|
|
83
|
+
export declare const createNotificationRequestSchema: z.ZodObject<{
|
|
84
|
+
userId: z.ZodString;
|
|
85
|
+
eventKey: z.ZodString;
|
|
86
|
+
title: z.ZodOptional<z.ZodString>;
|
|
87
|
+
body: z.ZodOptional<z.ZodString>;
|
|
88
|
+
channels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
89
|
+
emailTemplate: z.ZodOptional<z.ZodString>;
|
|
90
|
+
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
91
|
+
actionUrl: z.ZodOptional<z.ZodString>;
|
|
92
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
93
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
94
|
+
}, "strip", z.ZodTypeAny, {
|
|
95
|
+
userId: string;
|
|
96
|
+
eventKey: string;
|
|
97
|
+
title?: string | undefined;
|
|
98
|
+
body?: string | undefined;
|
|
99
|
+
channels?: string[] | undefined;
|
|
100
|
+
emailTemplate?: string | undefined;
|
|
101
|
+
data?: Record<string, unknown> | undefined;
|
|
102
|
+
actionUrl?: string | undefined;
|
|
103
|
+
tenantId?: string | undefined;
|
|
104
|
+
metadata?: Record<string, unknown> | undefined;
|
|
105
|
+
}, {
|
|
106
|
+
userId: string;
|
|
107
|
+
eventKey: string;
|
|
108
|
+
title?: string | undefined;
|
|
109
|
+
body?: string | undefined;
|
|
110
|
+
channels?: string[] | undefined;
|
|
111
|
+
emailTemplate?: string | undefined;
|
|
112
|
+
data?: Record<string, unknown> | undefined;
|
|
113
|
+
actionUrl?: string | undefined;
|
|
114
|
+
tenantId?: string | undefined;
|
|
115
|
+
metadata?: Record<string, unknown> | undefined;
|
|
116
|
+
}>;
|
|
117
|
+
export type CreateNotificationRequest = SendNotificationRequest;
|
|
118
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,oEAAoE;AACpE,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;EAKzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;EAAiC,CAAC;AAC5E,MAAM,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AAE/D,eAAO,MAAM,uBAAuB;;;;;;;;;EAGlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAgC,CAAC;AAC7E,MAAM,MAAM,yBAAyB,GAAG,uBAAuB,CAAC"}
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNotificationRequestSchema = exports.markAsReadRequestSchema = exports.getNotificationsRequestSchema = exports.listNotificationsRequestSchema = exports.sendNotificationRequestSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const nonEmptyStringSchema = zod_1.z.string().min(1);
|
|
6
|
+
const dataObjectSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
|
|
7
|
+
/** Payload for Kafka `notification.send.requested` / TCP create. */
|
|
8
|
+
exports.sendNotificationRequestSchema = zod_1.z.object({
|
|
9
|
+
userId: nonEmptyStringSchema,
|
|
10
|
+
eventKey: nonEmptyStringSchema,
|
|
11
|
+
title: zod_1.z.string().optional(),
|
|
12
|
+
body: zod_1.z.string().optional(),
|
|
13
|
+
channels: zod_1.z.array(zod_1.z.string()).optional(),
|
|
14
|
+
emailTemplate: zod_1.z.string().optional(),
|
|
15
|
+
data: dataObjectSchema.optional(),
|
|
16
|
+
actionUrl: zod_1.z.string().optional(),
|
|
17
|
+
tenantId: zod_1.z.string().optional(),
|
|
18
|
+
metadata: dataObjectSchema.optional(),
|
|
19
|
+
});
|
|
20
|
+
exports.listNotificationsRequestSchema = zod_1.z.object({
|
|
21
|
+
userId: nonEmptyStringSchema.optional(),
|
|
22
|
+
limit: zod_1.z.number().int().min(1).max(100).optional(),
|
|
23
|
+
offset: zod_1.z.number().int().min(0).optional(),
|
|
24
|
+
unreadOnly: zod_1.z.boolean().optional(),
|
|
25
|
+
});
|
|
26
|
+
exports.getNotificationsRequestSchema = exports.listNotificationsRequestSchema;
|
|
27
|
+
exports.markAsReadRequestSchema = zod_1.z.object({
|
|
28
|
+
id: nonEmptyStringSchema,
|
|
29
|
+
userId: nonEmptyStringSchema.optional(),
|
|
30
|
+
});
|
|
31
|
+
exports.createNotificationRequestSchema = exports.sendNotificationRequestSchema;
|
|
32
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAExB,MAAM,oBAAoB,GAAG,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/C,MAAM,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,MAAM,EAAE,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAE3D,oEAAoE;AACvD,QAAA,6BAA6B,GAAG,OAAC,CAAC,MAAM,CAAC;IACpD,MAAM,EAAE,oBAAoB;IAC5B,QAAQ,EAAE,oBAAoB;IAC9B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,QAAQ,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACxC,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACjC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC;AAGU,QAAA,8BAA8B,GAAG,OAAC,CAAC,MAAM,CAAC;IACrD,MAAM,EAAE,oBAAoB,CAAC,QAAQ,EAAE;IACvC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IAClD,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC1C,UAAU,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAGU,QAAA,6BAA6B,GAAG,sCAA8B,CAAC;AAG/D,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,oBAAoB;IACxB,MAAM,EAAE,oBAAoB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGU,QAAA,+BAA+B,GAAG,qCAA6B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Best practices — notification-service client
|
|
2
|
+
|
|
3
|
+
- **HTTP** for inbox list / mark-read / health / SSE from apps with `x-api-key`.
|
|
4
|
+
- **TCP** for internal Nest↔Nest or .NET↔Nest (`NotificationServiceTcpClient`).
|
|
5
|
+
- **Kafka** for async delivery (`NotificationService.Topics.SendRequested` → sent/failed).
|
|
6
|
+
- Configure with `NOTIFICATION_SERVICE_*` env or `NotificationServiceOptions` — any host/port.
|
|
7
|
+
- Keep TCP private; do not expose `TCP_PORT` publicly.
|
|
8
|
+
- Prefer Kafka for fan-out delivery; use TCP/HTTP for synchronous inbox ops.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Publish .NET SDK to nuget.org (Trusted Publishing)
|
|
2
|
+
|
|
3
|
+
No long-lived API key required.
|
|
4
|
+
|
|
5
|
+
→ [Trusted Publishing on nuget.org](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing)
|
|
6
|
+
|
|
7
|
+
## Quick setup
|
|
8
|
+
|
|
9
|
+
1. nuget.org → **Trusted Publishing** → policy:
|
|
10
|
+
- Repository owner: `yaghmori`
|
|
11
|
+
- Repository: `notification-service`
|
|
12
|
+
- Workflow: `cd-main.yml` (filename only)
|
|
13
|
+
2. GitHub → Settings → Secrets and variables → Actions → **Variables** → `NUGET_USER` = nuget.org **username** (not email, not API key).
|
|
14
|
+
3. CD uses `permissions.id-token: write` + `NuGet/login@v1` → temp key → `dotnet nuget push`.
|
|
15
|
+
|
|
16
|
+
Private repos: one successful login within 7 days permanently activates the policy.
|
|
17
|
+
|
|
18
|
+
After it works, revoke any old nuget.org API keys / `NUGET_TOKEN`.
|
|
19
|
+
|
|
20
|
+
Package id: `Yaghmori.NotificationService`.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Multi-protocol client guide (notification-service)
|
|
2
|
+
|
|
3
|
+
Integration guide for calling **notification-service** from **Node.js** and **.NET** over **HTTP**, **NestJS TCP**, and **Kafka**. Scope is notification inbox + multi-channel delivery.
|
|
4
|
+
|
|
5
|
+
| Artifact | Location |
|
|
6
|
+
|----------|----------|
|
|
7
|
+
| Truth file | `packages/client/contracts.json` |
|
|
8
|
+
| Node SDK | `@yaghmori/notification-service` (`packages/client`) |
|
|
9
|
+
| .NET SDK | `Yaghmori.NotificationService` (`sdk/dotnet/Yaghmori.NotificationService`) |
|
|
10
|
+
| Docker | `ghcr.io/yaghmori/notification-service` |
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd packages/client && pnpm run codegen
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Emits TS (`src/generated.ts`) and C# (`NotificationService.g.cs`).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Protocol selection
|
|
21
|
+
|
|
22
|
+
| Protocol | Sync? | Auth on wire | Typical callers | Network |
|
|
23
|
+
|----------|-------|--------------|-----------------|---------|
|
|
24
|
+
| **HTTP** | Yes | `x-api-key` / Bearer JWT | Apps, gateways, inbox UI | Public or private; authenticate unless `AUTH_DISABLED` |
|
|
25
|
+
| **NestJS TCP** | Request/response | None (trust network) | Nest / .NET mesh | Private VPC / mesh only |
|
|
26
|
+
| **Kafka** | Async | Cluster SASL/SSL | Fan-out delivery, retries, audit | Private Kafka; idempotent producers |
|
|
27
|
+
|
|
28
|
+
1. External clients → **HTTP + API key** (list / mark-read / health; SSE for live inbox).
|
|
29
|
+
2. Internal Nest / .NET → **TCP** for create / get / mark-read / archive.
|
|
30
|
+
3. “Deliver this notification eventually” → **Kafka** `notification.send.requested` (+ consume `sent` / `failed`).
|
|
31
|
+
|
|
32
|
+
Do **not** expose Nest TCP publicly. Use `x-api-key`, not `Authorization: Bearer <api-key>`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Address resolution
|
|
37
|
+
|
|
38
|
+
Package defaults (`ports.tcp = 4004`, `ports.http = 3000`) are **local compose suggestions**.
|
|
39
|
+
|
|
40
|
+
**HTTP base URL:** options `baseUrl` → `NOTIFICATION_SERVICE_URL` → `http://{HOST}:{HTTP_PORT|3000}`
|
|
41
|
+
|
|
42
|
+
**TCP:** options → `NOTIFICATION_SERVICE_HOST` + `NOTIFICATION_SERVICE_TCP_PORT` → `127.0.0.1:4004`
|
|
43
|
+
|
|
44
|
+
**Kafka:** options → `KAFKA_BROKERS` / `KAFKA_BOOTSTRAP_SERVERS` → `localhost:9092`
|
|
45
|
+
|
|
46
|
+
| Role | Variable |
|
|
47
|
+
|------|----------|
|
|
48
|
+
| Service HTTP listen | `PORT` |
|
|
49
|
+
| Service TCP listen | `TCP_PORT` |
|
|
50
|
+
| Client HTTP | `NOTIFICATION_SERVICE_URL` or host + `NOTIFICATION_SERVICE_HTTP_PORT` |
|
|
51
|
+
| Client TCP | `NOTIFICATION_SERVICE_HOST` + `NOTIFICATION_SERVICE_TCP_PORT` |
|
|
52
|
+
| Client auth | `NOTIFICATION_SERVICE_API_KEY` / `NOTIFICATION_SERVICE_BEARER` |
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
docker run --rm -p 8080:8080 -p 5004:5004 \
|
|
56
|
+
-e PORT=8080 -e TCP_PORT=5004 \
|
|
57
|
+
-e AUTH_API_KEYS=dev-key-1 \
|
|
58
|
+
ghcr.io/yaghmori/notification-service:latest
|
|
59
|
+
|
|
60
|
+
export NOTIFICATION_SERVICE_URL=http://127.0.0.1:8080
|
|
61
|
+
export NOTIFICATION_SERVICE_HOST=127.0.0.1
|
|
62
|
+
export NOTIFICATION_SERVICE_TCP_PORT=5004
|
|
63
|
+
export NOTIFICATION_SERVICE_API_KEY=dev-key-1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Authentication (HTTP)
|
|
69
|
+
|
|
70
|
+
| Mode | Config |
|
|
71
|
+
|------|--------|
|
|
72
|
+
| API keys | Admin keys or `AUTH_API_KEYS=k1,k2` → `x-api-key` / `Authorization: ApiKey …` |
|
|
73
|
+
| JWT | `JWT_SECRET` → `Authorization: Bearer <jwt>` |
|
|
74
|
+
| Disabled | `AUTH_DISABLED=true` — trusted mesh only |
|
|
75
|
+
| Public | `@Public()` e.g. `GET /health` |
|
|
76
|
+
|
|
77
|
+
TCP/Kafka use network trust + broker SASL, not `x-api-key`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 4. HTTP
|
|
82
|
+
|
|
83
|
+
| Constant | Path | Purpose |
|
|
84
|
+
|----------|------|---------|
|
|
85
|
+
| `LIST` | `GET /notifications` | Inbox list (query: userId, limit, offset, unreadOnly) |
|
|
86
|
+
| `STREAM` | `GET /notifications/stream` | SSE live feed |
|
|
87
|
+
| `MARK_READ` | `POST /notifications/{id}/read` | Mark one read |
|
|
88
|
+
| `MARK_READ_ALL` | `POST /notifications/read-all` | Mark all read |
|
|
89
|
+
| `ARCHIVE` / `ARCHIVE_ALL` | archive paths | Soft-remove from inbox |
|
|
90
|
+
| `DELETE` | `DELETE /notifications/{id}` | Delete |
|
|
91
|
+
| `HEALTH` | `GET /health` | Liveness |
|
|
92
|
+
|
|
93
|
+
Node SDK exposes `list`, `markRead`, `health`. .NET: `ListAsync`, `MarkReadAsync`, `HealthAsync`.
|
|
94
|
+
|
|
95
|
+
Timeouts: 10–30s for inbox APIs; health < 5s. Retry 408/429/5xx only.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 5. NestJS TCP
|
|
100
|
+
|
|
101
|
+
Framing: `<utf8-byte-length>#<json>`
|
|
102
|
+
|
|
103
|
+
Request: `{ "pattern": "notification.create_notification", "data": { … }, "id": "<uuid>" }`
|
|
104
|
+
Response: `{ "err": null, "response": { … }, "id": "<uuid>" }`
|
|
105
|
+
|
|
106
|
+
| Constant | Pattern |
|
|
107
|
+
|----------|---------|
|
|
108
|
+
| `GET_NOTIFICATIONS` | `notification.get_notifications` |
|
|
109
|
+
| `GET_NOTIFICATION_BY_ID` | `notification.get_notification_by_id` |
|
|
110
|
+
| `CREATE_NOTIFICATION` | `notification.create_notification` |
|
|
111
|
+
| `MARK_AS_READ` | `notification.mark_as_read` |
|
|
112
|
+
| `MARK_ALL_AS_READ` | `notification.mark_all_as_read` |
|
|
113
|
+
| `ARCHIVE_NOTIFICATION` | `notification.archive_notification` |
|
|
114
|
+
| `DELETE_NOTIFICATION` | `notification.delete_notification` |
|
|
115
|
+
| `HEALTH_CHECK` | `health.check` |
|
|
116
|
+
|
|
117
|
+
.NET helpers: `CreateAsync` / `GetNotificationsAsync` on `NotificationServiceTcpClient`. Prefer private networks; set connect + read timeouts.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 6. Kafka
|
|
122
|
+
|
|
123
|
+
| Role | Topic constant | Topic | Event type |
|
|
124
|
+
|------|----------------|-------|------------|
|
|
125
|
+
| Command | `SEND_REQUESTED` | `notification.send.requested` | `evt.notification.message.send.v1` |
|
|
126
|
+
| Lifecycle | `SENT` | `notification.sent` | `evt.notification.message.sent.v1` |
|
|
127
|
+
| Lifecycle | `FAILED` | `notification.failed` | `evt.notification.message.failed.v1` |
|
|
128
|
+
|
|
129
|
+
**Send payload** (camelCase JSON):
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"userId": "user-1",
|
|
134
|
+
"eventKey": "order.shipped",
|
|
135
|
+
"title": "Order shipped",
|
|
136
|
+
"body": "Your order is on the way.",
|
|
137
|
+
"channels": ["in-app", "email"],
|
|
138
|
+
"emailTemplate": "optional-template",
|
|
139
|
+
"data": { "orderId": "1" },
|
|
140
|
+
"actionUrl": "https://app.example.com/orders/1",
|
|
141
|
+
"tenantId": "optional",
|
|
142
|
+
"metadata": {}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Producer defaults: `acks=all`, idempotence, retries ≥ 3, headers `content-type` + `x-event-type`.
|
|
147
|
+
Consumer: manual commit after side effects; dedicated `group.id`.
|
|
148
|
+
|
|
149
|
+
Kafka env: `KAFKA_BROKERS`, `KAFKA_CLIENT_ID`, `KAFKA_SSL`, `KAFKA_SASL_*` (aliases `KAFKA_USERNAME` / `KAFKA_PASSWORD`).
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 7. Node.js
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import {
|
|
157
|
+
createNotificationHttpClient,
|
|
158
|
+
notificationTcpClient,
|
|
159
|
+
NotificationKafka,
|
|
160
|
+
NotificationService,
|
|
161
|
+
} from '@yaghmori/notification-service';
|
|
162
|
+
|
|
163
|
+
const http = createNotificationHttpClient({
|
|
164
|
+
baseUrl: process.env.NOTIFICATION_SERVICE_URL,
|
|
165
|
+
auth: { apiKey: process.env.NOTIFICATION_SERVICE_API_KEY! },
|
|
166
|
+
});
|
|
167
|
+
await http.list({ userId: 'u1' });
|
|
168
|
+
await http.markRead(id);
|
|
169
|
+
await http.health();
|
|
170
|
+
|
|
171
|
+
// Nest: ClientsModule.register([notificationTcpClient()])
|
|
172
|
+
// Kafka: NotificationKafka.sendRequested + NotificationKafka.connection()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 8. .NET
|
|
178
|
+
|
|
179
|
+
```csharp
|
|
180
|
+
services.AddNotificationServiceClient(o =>
|
|
181
|
+
{
|
|
182
|
+
o.ApiKey = Environment.GetEnvironmentVariable("NOTIFICATION_SERVICE_API_KEY");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
using var http = new NotificationServiceHttpClient(new NotificationServiceOptions());
|
|
186
|
+
await http.ListAsync(userId: "u1");
|
|
187
|
+
await http.MarkReadAsync(id);
|
|
188
|
+
await http.HealthAsync();
|
|
189
|
+
|
|
190
|
+
await using var tcp = new NotificationServiceTcpClient(new NotificationServiceOptions());
|
|
191
|
+
await tcp.CreateAsync(new SendNotificationRequest
|
|
192
|
+
{
|
|
193
|
+
UserId = "u1",
|
|
194
|
+
EventKey = "order.shipped",
|
|
195
|
+
Title = "Order shipped",
|
|
196
|
+
Body = "On the way",
|
|
197
|
+
});
|
|
198
|
+
await tcp.GetNotificationsAsync(new { userId = "u1" });
|
|
199
|
+
|
|
200
|
+
using var kafka = new NotificationKafkaProducer(new NotificationKafkaOptions());
|
|
201
|
+
await kafka.PublishSendRequestedAsync(new SendNotificationRequest
|
|
202
|
+
{
|
|
203
|
+
UserId = "u1",
|
|
204
|
+
EventKey = "order.shipped",
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Constants live in generated `NotificationService.g.cs` (`Patterns`, `HttpPaths`, `Topics`, `EventTypes`).
|
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Full SDK usage — @yaghmori/notification-service
|
|
2
|
+
|
|
3
|
+
HTTP inbox + TCP RPC + Kafka send topics + auth. Deploy on **any host/port**; configure the SDK.
|
|
4
|
+
|
|
5
|
+
## Install / run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @yaghmori/notification-service zod
|
|
9
|
+
docker pull ghcr.io/yaghmori/notification-service:latest
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
docker run --rm -p 3000:3000 -p 4004:4004 \
|
|
14
|
+
-e PORT=3000 -e TCP_PORT=4004 \
|
|
15
|
+
-e AUTH_API_KEYS=dev-key \
|
|
16
|
+
-e DATABASE_URL=... \
|
|
17
|
+
ghcr.io/yaghmori/notification-service:latest
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configure address / ports
|
|
21
|
+
|
|
22
|
+
| | Client options | Client env | Service env |
|
|
23
|
+
|--|----------------|------------|-------------|
|
|
24
|
+
| HTTP | `baseUrl` / `host`+`port` | `NOTIFICATION_SERVICE_URL` or `HOST`+`HTTP_PORT` | `PORT` |
|
|
25
|
+
| TCP | `notificationTcpClient({ host, port })` | `NOTIFICATION_SERVICE_HOST`, `NOTIFICATION_SERVICE_TCP_PORT` | `TCP_PORT` |
|
|
26
|
+
| Kafka | `NotificationKafka.connection()` | `KAFKA_BROKERS`, SASL_*, SSL | same |
|
|
27
|
+
|
|
28
|
+
## Auth (HTTP)
|
|
29
|
+
|
|
30
|
+
| Mode | Service config | Client |
|
|
31
|
+
|------|----------------|--------|
|
|
32
|
+
| API key | `AUTH_API_KEYS=k1,k2` | `auth: { apiKey }` → `x-api-key` |
|
|
33
|
+
| JWT | `JWT_SECRET` + Bearer with `serviceName` | `auth: { bearerToken }` |
|
|
34
|
+
| Open | `AUTH_DISABLED=true` | omit auth |
|
|
35
|
+
| Health | always public | `GET /health` |
|
|
36
|
+
|
|
37
|
+
TCP: private network. Kafka: broker SASL/SSL via env (not HTTP API keys).
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createNotificationHttpClient } from '@yaghmori/notification-service';
|
|
41
|
+
|
|
42
|
+
const notifications = createNotificationHttpClient({
|
|
43
|
+
baseUrl: process.env.NOTIFICATION_SERVICE_URL,
|
|
44
|
+
auth: { apiKey: process.env.NOTIFICATION_SERVICE_API_KEY! },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## HTTP
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await notifications.list({ userId: 'user-1', unreadOnly: true, limit: 20 });
|
|
52
|
+
await notifications.markRead(notificationId);
|
|
53
|
+
await notifications.health();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Paths: `NotificationService.httpPaths.LIST` → `/notifications`, etc. SSE stream is `STREAM` → `/notifications/stream` (wire your own EventSource; HTTP client focuses on list/mark-read/health).
|
|
57
|
+
|
|
58
|
+
## NestJS TCP
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { Module, Inject, Injectable } from '@nestjs/common';
|
|
62
|
+
import { ClientsModule, ClientProxy } from '@nestjs/microservices';
|
|
63
|
+
import {
|
|
64
|
+
notificationTcpClient,
|
|
65
|
+
NotificationService,
|
|
66
|
+
PATTERNS,
|
|
67
|
+
} from '@yaghmori/notification-service';
|
|
68
|
+
import { firstValueFrom } from 'rxjs';
|
|
69
|
+
|
|
70
|
+
@Module({
|
|
71
|
+
imports: [
|
|
72
|
+
ClientsModule.register([
|
|
73
|
+
notificationTcpClient({
|
|
74
|
+
host: process.env.NOTIFICATION_SERVICE_HOST,
|
|
75
|
+
port: Number(process.env.NOTIFICATION_SERVICE_TCP_PORT) || 4004,
|
|
76
|
+
}),
|
|
77
|
+
]),
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
export class AppModule {}
|
|
81
|
+
|
|
82
|
+
@Injectable()
|
|
83
|
+
export class NotifyFacade {
|
|
84
|
+
constructor(@Inject(NotificationService.token) private readonly client: ClientProxy) {}
|
|
85
|
+
|
|
86
|
+
create(payload: {
|
|
87
|
+
userId: string;
|
|
88
|
+
eventKey: string;
|
|
89
|
+
title?: string;
|
|
90
|
+
body?: string;
|
|
91
|
+
}) {
|
|
92
|
+
return firstValueFrom(
|
|
93
|
+
this.client.send(PATTERNS.CREATE_NOTIFICATION, payload),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
list(userId: string) {
|
|
98
|
+
return firstValueFrom(
|
|
99
|
+
this.client.send(PATTERNS.GET_NOTIFICATIONS, { userId }),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Kafka — request a send
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { Kafka } from 'kafkajs';
|
|
109
|
+
import {
|
|
110
|
+
NotificationKafka,
|
|
111
|
+
sendNotificationRequestSchema,
|
|
112
|
+
} from '@yaghmori/notification-service';
|
|
113
|
+
|
|
114
|
+
const conn = NotificationKafka.connection();
|
|
115
|
+
const kafka = new Kafka(conn);
|
|
116
|
+
const producer = kafka.producer();
|
|
117
|
+
await producer.connect();
|
|
118
|
+
|
|
119
|
+
const payload = sendNotificationRequestSchema.parse({
|
|
120
|
+
userId: 'user-1',
|
|
121
|
+
eventKey: 'order.shipped',
|
|
122
|
+
title: 'Order shipped',
|
|
123
|
+
body: 'Your order is on the way.',
|
|
124
|
+
channels: ['in-app', 'email'],
|
|
125
|
+
actionUrl: 'https://app.example.com/orders/1',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await producer.send({
|
|
129
|
+
topic: NotificationKafka.sendRequested, // notification.send.requested
|
|
130
|
+
messages: [
|
|
131
|
+
{
|
|
132
|
+
key: payload.userId,
|
|
133
|
+
value: JSON.stringify(payload),
|
|
134
|
+
headers: {
|
|
135
|
+
'content-type': 'application/json',
|
|
136
|
+
'x-event-type': NotificationKafka.eventTypes.SEND,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Lifecycle topics: `notification.sent`, `notification.failed`.
|
|
144
|
+
|
|
145
|
+
## Constants
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { NotificationService } from '@yaghmori/notification-service';
|
|
149
|
+
|
|
150
|
+
NotificationService.ports; // { tcp: 4004, http: 3000 }
|
|
151
|
+
NotificationService.patterns.CREATE_NOTIFICATION;
|
|
152
|
+
NotificationService.topics.SEND_REQUESTED;
|
|
153
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yaghmori/notification-service",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Client SDK for notification-service — inbox HTTP/SSE, Nest TCP patterns, Kafka send topics, Zod DTOs",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./nest": {
|
|
15
|
+
"types": "./dist/nest.d.ts",
|
|
16
|
+
"default": "./dist/nest.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"contracts.json",
|
|
22
|
+
"README.md",
|
|
23
|
+
"docs"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@nestjs/common": "^11.0.0",
|
|
30
|
+
"@nestjs/microservices": "^11.0.0",
|
|
31
|
+
"rxjs": "^7.8.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@nestjs/common": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"@nestjs/microservices": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"rxjs": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@nestjs/common": "^11.0.0",
|
|
46
|
+
"@nestjs/microservices": "^11.0.0",
|
|
47
|
+
"@types/node": "^22.15.3",
|
|
48
|
+
"rxjs": "^7.8.0",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/yaghmori/notification-service.git",
|
|
58
|
+
"directory": "packages/client"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"codegen": "node scripts/codegen.cjs",
|
|
65
|
+
"build": "pnpm run codegen && tsc -p tsconfig.json",
|
|
66
|
+
"check-types": "tsc -p tsconfig.json --noEmit"
|
|
67
|
+
}
|
|
68
|
+
}
|