cds-error-outbox 1.0.0 → 1.0.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 CHANGED
@@ -1,19 +1,39 @@
1
- # cds-error-outbox
1
+ <div align="center">
2
2
 
3
- A production-ready, reusable **SAP CAP** (Node.js) plugin that automatically captures service errors, deduplicates them, and sends batched HTML email notifications — with **zero production dependencies**.
3
+ # CDS Error Outbox
4
+
5
+ **Automatic error capture, deduplication & email alerting for SAP CAP applications.**
6
+
7
+ <br>
8
+
9
+ [![npm](https://img.shields.io/npm/v/cds-error-outbox?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/cds-error-outbox)
10
+ [![SAP CAP](https://img.shields.io/badge/SAP%20CAP-plugin-009FDB?style=for-the-badge&logo=sap&logoColor=white)](https://cap.cloud.sap)
11
+ [![MIT](https://img.shields.io/badge/license-MIT-22c55e?style=for-the-badge&logo=opensourceinitiative&logoColor=white)](./LICENSE)
12
+ [![zero deps](https://img.shields.io/badge/deps-zero-6366f1?style=for-the-badge&logo=nodedotjs&logoColor=white)](#)
13
+
14
+ [![Microsoft O365](https://img.shields.io/badge/Microsoft%20365-mail%20ready-0078D4?style=flat-square&logo=microsoft&logoColor=white)](#o365--microsoft-graph-api)
15
+ [![SMTP](https://img.shields.io/badge/SMTP-nodemailer-EA4335?style=flat-square&logo=gmail&logoColor=white)](#smtp)
16
+ [![SQLite](https://img.shields.io/badge/SQLite-supported-003B57?style=flat-square&logo=sqlite&logoColor=white)](#db-entity)
17
+ [![SAP HANA](https://img.shields.io/badge/SAP%20HANA-supported-1B6B3A?style=flat-square&logo=sap&logoColor=white)](#db-entity)
18
+ [![SAP BTP](https://img.shields.io/badge/SAP%20BTP-Cloud%20Foundry-0FAAFF?style=flat-square&logo=sap&logoColor=white)](#environment-variables)
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ **[Installation](#installation) · [Configuration](#configuration) · [Email Providers](#email-providers) · [How it works](#how-it-works) · [DB Entity](#db-entity) · [Environment variables](#environment-variables) · [Project structure](#project-structure)**
4
25
 
5
26
  ---
6
27
 
7
28
  ## Features
8
29
 
9
- - Hooks into **all CAP services** via `srv.on('error')` — zero manual wiring required
10
- - Stores errors in a CDS-managed `error.outbox.Errors` DB entity (auto-deployed)
11
- - **Deduplicates** by `SHA-256(message + service + action)` — increments count instead of creating duplicate rows
30
+ - Hooks into **all CAP services** automatically — zero manual wiring required
31
+ - Persists errors to a CDS-managed `error.outbox.Errors` entity
32
+ - **Deduplicates** via `SHA-256(message + service + action)` — increments a counter instead of flooding the DB
12
33
  - Sends **batched HTML email reports** on a configurable interval
13
- - Pluggable email providers: **O365 (Microsoft Graph API)**, **SMTP (nodemailer)**, **Mock (dev/test)**
14
- - Fully configurable via `cds.env.requires.errorOutbox`
15
- - **Non-blocking** — error capture is fire-and-forget; the request pipeline is never delayed
16
- - Never crashes the application — all internal failures are logged and swallowed
34
+ - Pluggable providers: **O365 (Microsoft Graph)**, **SMTP (nodemailer)**, **Mock (dev/test)**
35
+ - **Non-blocking** fire-and-forget capture, the request pipeline is never delayed
36
+ - **Resilient** — all internal failures are caught, logged, and swallowed; the app never crashes
17
37
 
18
38
  ---
19
39
 
@@ -25,12 +45,33 @@ npm install cds-error-outbox
25
45
 
26
46
  Because `package.json` declares `"cds": { "plugin": true }`, CAP automatically loads `index.js` at startup.
27
47
 
28
- > **Important:** Due to how CAP resolves symlinked local packages, you must explicitly require the plugin in your project's `srv/server.js` (or root `server.js`):
48
+ > **Note:** Due to how CAP resolves symlinked local packages, you may need to explicitly require the plugin so it is loaded at startup.
29
49
  >
50
+ > **Option A — with a custom `server.js`:**
30
51
  > ```js
31
- > require('cds-error-outbox'); // ← add as the very first line
52
+ > require("cds-error-outbox"); // ← add as the very first line
32
53
  > // ... rest of your server.js
33
54
  > ```
55
+ >
56
+ > **Option B — without a custom `server.js`** (most common):
57
+ > Add the require at the top of any service file, e.g. `srv/admin-service.js`:
58
+ > ```js
59
+ > require("cds-error-outbox"); // ← add at the top
60
+ > // ... rest of your service
61
+ > ```
62
+
63
+ Then register the DB model in your project (e.g. in `db/schema.cds`):
64
+
65
+ ```cds
66
+ using from 'cds-error-outbox/db/model';
67
+ ```
68
+
69
+ And deploy:
70
+
71
+ ```bash
72
+ cds deploy --to sqlite # local development
73
+ cds build # production (BTP, HANA)
74
+ ```
34
75
 
35
76
  ---
36
77
 
@@ -66,24 +107,24 @@ Add the following to your project's `package.json` under `cds.requires`, or to `
66
107
 
67
108
  ### Configuration reference
68
109
 
69
- | Key | Type | Default | Description |
70
- |---|---|---|---|
71
- | `enabled` | boolean | `true` | Enable/disable the entire plugin |
72
- | `interval` | number (ms) | `300000` | Batch email job frequency |
73
- | `batchSize` | number | `50` | Max errors per email batch |
74
- | `dedup.enabled` | boolean | `true` | Enable hash-based deduplication |
75
- | `dedup.windowMinutes` | number | `10` | Rolling dedup window in minutes |
76
- | `mail.provider` | string | `'mock'` | `'o365'` \| `'smtp'` \| `'mock'` |
77
- | `mail.from` | string | `''` | Sender address |
78
- | `mail.to` | string | `''` | Recipient(s), comma-separated |
79
- | `mail.tenantId` | string | `''` | Azure AD tenant ID (O365 only) |
80
- | `mail.clientId` | string | `''` | Azure AD app client ID (O365 only) |
81
- | `mail.clientSecret` | string | `''` | Azure AD app client secret (O365 only) |
82
- | `mail.smtp.host` | string | `''` | SMTP host (SMTP only) |
83
- | `mail.smtp.port` | number | `587` | SMTP port (SMTP only) |
84
- | `mail.smtp.secure` | boolean | `false` | Use TLS (SMTP only) |
85
- | `mail.smtp.auth.user` | string | `''` | SMTP username (SMTP only) |
86
- | `mail.smtp.auth.pass` | string | `''` | SMTP password (SMTP only) |
110
+ | Key | Type | Default | Description |
111
+ | --------------------- | ----------- | -------- | -------------------------------------- |
112
+ | `enabled` | boolean | `true` | Enable/disable the entire plugin |
113
+ | `interval` | number (ms) | `300000` | Batch email job frequency |
114
+ | `batchSize` | number | `50` | Max errors per email batch |
115
+ | `dedup.enabled` | boolean | `true` | Enable hash-based deduplication |
116
+ | `dedup.windowMinutes` | number | `10` | Rolling dedup window in minutes |
117
+ | `mail.provider` | string | `'mock'` | `'o365'` \| `'smtp'` \| `'mock'` |
118
+ | `mail.from` | string | `''` | Sender address |
119
+ | `mail.to` | string | `''` | Recipient(s), comma-separated |
120
+ | `mail.tenantId` | string | `''` | Azure AD tenant ID (O365 only) |
121
+ | `mail.clientId` | string | `''` | Azure AD app client ID (O365 only) |
122
+ | `mail.clientSecret` | string | `''` | Azure AD app client secret (O365 only) |
123
+ | `mail.smtp.host` | string | `''` | SMTP host (SMTP only) |
124
+ | `mail.smtp.port` | number | `587` | SMTP port (SMTP only) |
125
+ | `mail.smtp.secure` | boolean | `false` | Use TLS (SMTP only) |
126
+ | `mail.smtp.auth.user` | string | `''` | SMTP username (SMTP only) |
127
+ | `mail.smtp.auth.pass` | string | `''` | SMTP password (SMTP only) |
87
128
 
88
129
  ---
89
130
 
@@ -163,7 +204,7 @@ npm install nodemailer
163
204
  CAP service throws an error
164
205
 
165
206
 
166
- srv.on('error') — fire-and-forget via setImmediate (non-blocking)
207
+ srv.handle() wrapper — fire-and-forget via setImmediate (non-blocking)
167
208
 
168
209
 
169
210
  SHA-256( message | service | action )
@@ -216,19 +257,6 @@ entity Errors {
216
257
  }
217
258
  ```
218
259
 
219
- After installing the plugin, register the model in your project's `db/` folder. Create a file `db/error-outbox.cds`:
220
-
221
- ```cds
222
- using from '../plugins/cds-error-outbox/db/model';
223
- ```
224
-
225
- Then run deploy:
226
-
227
- ```bash
228
- cds deploy --to sqlite # local development
229
- cds build # production (BTP, HANA)
230
- ```
231
-
232
260
  ---
233
261
 
234
262
  ## Environment variables
@@ -261,7 +289,7 @@ cds-error-outbox/
261
289
  ├── lib/
262
290
  │ ├── bootstrap.js ← Init orchestration (cds lifecycle hooks)
263
291
  │ ├── config.js ← Deep merge + config loader (singleton)
264
- │ ├── interceptor.js ← srv.on('error') hook (fire-and-forget)
292
+ │ ├── interceptor.js ← srv.handle() wrapper (fire-and-forget)
265
293
  │ ├── dedup.js ← SHA-256 hash + DB upsert logic
266
294
  │ ├── scheduler.js ← Interval batch job
267
295
  │ └── formatter.js ← HTML email builder
@@ -12,37 +12,48 @@ const { upsertError } = require('./dedup');
12
12
  * @param {object} config - merged plugin config
13
13
  */
14
14
  function attach(srv, config) {
15
- srv.on('error', (err, req) => {
16
- // setImmediate defers DB work to after the current event loop tick,
17
- // ensuring the error response is sent to the client without any delay.
18
- setImmediate(async () => {
19
- try {
20
- const message = (err && err.message) ? String(err.message) : String(err);
21
- const stack = (err && err.stack) ? String(err.stack) : '';
22
- const service = srv.name || 'unknown';
23
- const action =
24
- (req && req.event) ? req.event :
25
- (req && req.path) ? req.path :
26
- (req && req.method) ? req.method :
27
- 'unknown';
15
+ // CAP's srv.handle() is the correct override point — it runs before/on/after
16
+ // handlers and any error thrown there propagates out of handle().
17
+ // dispatch() causes infinite recursion (it calls this.dispatch on sub-tx),
18
+ // srv.on('*') never fires when a before-handler throws.
19
+ // The CAP docs say: "Subclasses should overload handle instead of dispatch".
20
+ const _handle = srv.handle.bind(srv);
21
+ srv.handle = async function (req) {
22
+ try {
23
+ return await _handle(req);
24
+ } catch (err) {
25
+ // Persist asynchronously — never block or swallow the error.
26
+ setImmediate(async () => {
27
+ try {
28
+ const message = (err && err.message) ? String(err.message) : String(err);
29
+ const stack = (err && err.stack) ? String(err.stack) : '';
30
+ const service = srv.name || 'unknown';
31
+ const action =
32
+ (req && req.event) ? req.event :
33
+ (req && req.path) ? req.path :
34
+ (req && req.method) ? req.method :
35
+ 'unknown';
28
36
 
29
- // cds.tx() opens a brand-new root-level transaction that is completely
30
- // independent of the failed request's already-rolled-back transaction.
31
- // Without this, CAP's AsyncLocalStorage would propagate the dead
32
- // transaction context into our INSERT/UPDATE, causing the
33
- // "Transaction is rolled back" error.
34
- await cds.tx(async (tx) => {
35
- await upsertError(tx, { message, stack, service, action }, config);
36
- });
37
- } catch (internalError) {
38
- // Never re-throw — log internally to avoid crashing the app.
39
- console.error(
40
- `[cds-error-outbox] Failed to persist error from service "${srv.name}":`,
41
- internalError.message
42
- );
43
- }
44
- });
45
- });
37
+ // cds.tx() opens a brand-new root-level transaction that is completely
38
+ // independent of the failed request's already-rolled-back transaction.
39
+ // Without this, CAP's AsyncLocalStorage would propagate the dead
40
+ // transaction context into our INSERT/UPDATE, causing the
41
+ // "Transaction is rolled back" error.
42
+ await cds.tx(async (tx) => {
43
+ await upsertError(tx, { message, stack, service, action }, config);
44
+ });
45
+ } catch (internalError) {
46
+ // Never re-throw — log internally to avoid crashing the app.
47
+ console.error(
48
+ `[cds-error-outbox] Failed to persist error from service "${srv.name}":`,
49
+ internalError.message
50
+ );
51
+ }
52
+ });
53
+
54
+ throw err; // re-throw so CAP still sends the error response to the client
55
+ }
56
+ };
46
57
  }
47
58
 
48
59
  module.exports = { attach };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cds-error-outbox",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A reusable CAP plugin that captures service errors, deduplicates them, and sends batched email notifications.",
5
5
  "main": "index.js",
6
6
  "cds": {