cds-error-outbox 1.0.0 → 1.0.2

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/lib/scheduler.js CHANGED
@@ -1,125 +1,125 @@
1
- 'use strict';
2
-
3
- const cds = require('@sap/cds');
4
- const { formatHtmlEmail } = require('./formatter');
5
-
6
- const ENTITY = 'error.outbox.Errors';
7
-
8
- let _handle = null;
9
-
10
- /**
11
- * Start the interval-based batch scheduler.
12
- *
13
- * @param {object} config - merged plugin config
14
- * @param {object} provider - email provider instance ({ send: Function })
15
- * @returns {NodeJS.Timeout} interval handle (can be passed to stop())
16
- */
17
- function start(config, provider) {
18
- if (_handle) {
19
- clearInterval(_handle);
20
- _handle = null;
21
- }
22
-
23
- _handle = setInterval(() => {
24
- runBatch(config, provider).catch((err) => {
25
- // runBatch already guards internally; this is a last-resort safety net.
26
- console.error('[cds-error-outbox] Unhandled error in runBatch:', err.message);
27
- });
28
- }, config.interval);
29
-
30
- // Allow Node.js process to exit gracefully even if the interval is active.
31
- if (typeof _handle.unref === 'function') _handle.unref();
32
-
33
- console.info(
34
- `[cds-error-outbox] Scheduler started — ` +
35
- `interval: ${config.interval}ms, batchSize: ${config.batchSize}`
36
- );
37
-
38
- return _handle;
39
- }
40
-
41
- /**
42
- * Stop the scheduler. Safe to call even if not started.
43
- */
44
- function stop() {
45
- if (_handle) {
46
- clearInterval(_handle);
47
- _handle = null;
48
- console.info('[cds-error-outbox] Scheduler stopped.');
49
- }
50
- }
51
-
52
- /**
53
- * Core batch routine:
54
- * 1. Fetch unsent errors from DB (up to batchSize, oldest first).
55
- * 2. Format them into an HTML email.
56
- * 3. Send via the configured email provider.
57
- * 4. Mark successfully sent records as sent=true.
58
- *
59
- * Each step is independently guarded so a failure in one step does not
60
- * prevent the scheduler from running again on the next interval.
61
- *
62
- * @param {object} config
63
- * @param {object} provider
64
- */
65
- async function runBatch(config, provider) {
66
- // ── 1. Obtain DB connection ──────────────────────────────────────────────
67
- let db;
68
- try {
69
- db = await cds.connect.to('db');
70
- } catch (err) {
71
- console.error('[cds-error-outbox] Cannot connect to DB — skipping batch:', err.message);
72
- return;
73
- }
74
-
75
- // ── 2. Fetch unsent errors ───────────────────────────────────────────────
76
- let errors;
77
- try {
78
- errors = await db.run(
79
- SELECT.from(ENTITY)
80
- .where({ sent: false })
81
- .orderBy('lastSeen asc')
82
- .limit(config.batchSize)
83
- );
84
- } catch (err) {
85
- console.error('[cds-error-outbox] Failed to query unsent errors:', err.message);
86
- return;
87
- }
88
-
89
- if (!errors || errors.length === 0) return;
90
-
91
- // ── 3. Format email ──────────────────────────────────────────────────────
92
- let subject, html;
93
- try {
94
- ({ subject, html } = formatHtmlEmail(errors));
95
- } catch (err) {
96
- console.error('[cds-error-outbox] Failed to format email body:', err.message);
97
- return;
98
- }
99
-
100
- // ── 4. Send email ────────────────────────────────────────────────────────
101
- try {
102
- await provider.send(config.mail, subject, html);
103
- } catch (err) {
104
- // Do NOT mark as sent — will be retried on the next interval.
105
- console.error(
106
- `[cds-error-outbox] Email delivery failed (will retry on next interval): ${err.message}`
107
- );
108
- return;
109
- }
110
-
111
- // ── 5. Mark as sent (only after confirmed delivery) ──────────────────────
112
- const ids = errors.map((e) => e.ID);
113
- try {
114
- await db.run(
115
- UPDATE(ENTITY)
116
- .set({ sent: true })
117
- .where({ ID: { in: ids } })
118
- );
119
- console.info(`[cds-error-outbox] Batch complete — marked ${ids.length} error(s) as sent.`);
120
- } catch (err) {
121
- console.error('[cds-error-outbox] Failed to mark errors as sent:', err.message);
122
- }
123
- }
124
-
125
- module.exports = { start, stop, runBatch };
1
+ 'use strict';
2
+
3
+ const cds = require('@sap/cds');
4
+ const { formatHtmlEmail } = require('./formatter');
5
+
6
+ const ENTITY = 'error.outbox.Errors';
7
+
8
+ let _handle = null;
9
+
10
+ /**
11
+ * Start the interval-based batch scheduler.
12
+ *
13
+ * @param {object} config - merged plugin config
14
+ * @param {object} provider - email provider instance ({ send: Function })
15
+ * @returns {NodeJS.Timeout} interval handle (can be passed to stop())
16
+ */
17
+ function start(config, provider) {
18
+ if (_handle) {
19
+ clearInterval(_handle);
20
+ _handle = null;
21
+ }
22
+
23
+ _handle = setInterval(() => {
24
+ runBatch(config, provider).catch((err) => {
25
+ // runBatch already guards internally; this is a last-resort safety net.
26
+ console.error('[cds-error-outbox] Unhandled error in runBatch:', err.message);
27
+ });
28
+ }, config.interval);
29
+
30
+ // Allow Node.js process to exit gracefully even if the interval is active.
31
+ if (typeof _handle.unref === 'function') _handle.unref();
32
+
33
+ console.info(
34
+ `[cds-error-outbox] Scheduler started — ` +
35
+ `interval: ${config.interval}ms, batchSize: ${config.batchSize}`
36
+ );
37
+
38
+ return _handle;
39
+ }
40
+
41
+ /**
42
+ * Stop the scheduler. Safe to call even if not started.
43
+ */
44
+ function stop() {
45
+ if (_handle) {
46
+ clearInterval(_handle);
47
+ _handle = null;
48
+ console.info('[cds-error-outbox] Scheduler stopped.');
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Core batch routine:
54
+ * 1. Fetch unsent errors from DB (up to batchSize, oldest first).
55
+ * 2. Format them into an HTML email.
56
+ * 3. Send via the configured email provider.
57
+ * 4. Mark successfully sent records as sent=true.
58
+ *
59
+ * Each step is independently guarded so a failure in one step does not
60
+ * prevent the scheduler from running again on the next interval.
61
+ *
62
+ * @param {object} config
63
+ * @param {object} provider
64
+ */
65
+ async function runBatch(config, provider) {
66
+ // ── 1. Obtain DB connection ──────────────────────────────────────────────
67
+ let db;
68
+ try {
69
+ db = await cds.connect.to('db');
70
+ } catch (err) {
71
+ console.error('[cds-error-outbox] Cannot connect to DB — skipping batch:', err.message);
72
+ return;
73
+ }
74
+
75
+ // ── 2. Fetch unsent errors ───────────────────────────────────────────────
76
+ let errors;
77
+ try {
78
+ errors = await db.run(
79
+ SELECT.from(ENTITY)
80
+ .where({ sent: false })
81
+ .orderBy('lastSeen asc')
82
+ .limit(config.batchSize)
83
+ );
84
+ } catch (err) {
85
+ console.error('[cds-error-outbox] Failed to query unsent errors:', err.message);
86
+ return;
87
+ }
88
+
89
+ if (!errors || errors.length === 0) return;
90
+
91
+ // ── 3. Format email ──────────────────────────────────────────────────────
92
+ let subject, html;
93
+ try {
94
+ ({ subject, html } = formatHtmlEmail(errors));
95
+ } catch (err) {
96
+ console.error('[cds-error-outbox] Failed to format email body:', err.message);
97
+ return;
98
+ }
99
+
100
+ // ── 4. Send email ────────────────────────────────────────────────────────
101
+ try {
102
+ await provider.send(config.mail, subject, html);
103
+ } catch (err) {
104
+ // Do NOT mark as sent — will be retried on the next interval.
105
+ console.error(
106
+ `[cds-error-outbox] Email delivery failed (will retry on next interval): ${err.message}`
107
+ );
108
+ return;
109
+ }
110
+
111
+ // ── 5. Mark as sent (only after confirmed delivery) ──────────────────────
112
+ const ids = errors.map((e) => e.ID);
113
+ try {
114
+ await db.run(
115
+ UPDATE(ENTITY)
116
+ .set({ sent: true })
117
+ .where({ ID: { in: ids } })
118
+ );
119
+ console.info(`[cds-error-outbox] Batch complete — marked ${ids.length} error(s) as sent.`);
120
+ } catch (err) {
121
+ console.error('[cds-error-outbox] Failed to mark errors as sent:', err.message);
122
+ }
123
+ }
124
+
125
+ module.exports = { start, stop, runBatch };
package/package.json CHANGED
@@ -1,34 +1,40 @@
1
- {
2
- "name": "cds-error-outbox",
3
- "version": "1.0.0",
4
- "description": "A reusable CAP plugin that captures service errors, deduplicates them, and sends batched email notifications.",
5
- "main": "index.js",
6
- "cds": {
7
- "plugin": true,
8
- "requires": {
9
- "cds-error-outbox": {
10
- "model": "cds-error-outbox"
11
- }
12
- }
13
- },
14
- "keywords": [
15
- "sap-cap",
16
- "cds",
17
- "plugin",
18
- "error",
19
- "outbox",
20
- "email"
21
- ],
22
- "license": "MIT",
23
- "engines": {
24
- "node": ">=18"
25
- },
26
- "peerDependencies": {
27
- "@sap/cds": ">=7"
28
- },
29
- "peerDependenciesMeta": {
30
- "nodemailer": {
31
- "optional": true
32
- }
33
- }
34
- }
1
+ {
2
+ "name": "cds-error-outbox",
3
+ "version": "1.0.2",
4
+ "description": "A reusable CAP plugin that captures service errors, deduplicates them, and sends batched email notifications.",
5
+ "main": "index.js",
6
+ "cds": {
7
+ "plugin": true,
8
+ "requires": {
9
+ "cds-error-outbox": {
10
+ "model": "cds-error-outbox"
11
+ }
12
+ }
13
+ },
14
+ "keywords": [
15
+ "sap-cap",
16
+ "cds",
17
+ "plugin",
18
+ "error",
19
+ "outbox",
20
+ "email"
21
+ ],
22
+ "license": "MIT",
23
+ "scripts": {
24
+ "test": "node --test tests/**/*.test.js"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "peerDependencies": {
30
+ "@sap/cds": ">=7"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "nodemailer": {
34
+ "optional": true
35
+ }
36
+ },
37
+ "devDependencies": {
38
+ "@sap/cds": "^9.9.1"
39
+ }
40
+ }
@@ -1,40 +1,40 @@
1
- 'use strict';
2
-
3
- /**
4
- * Email provider factory.
5
- *
6
- * Returns a provider object with a `send(mailConfig, subject, html)` function
7
- * based on config.mail.provider. Falls back to 'mock' for unknown values.
8
- *
9
- * Supported providers:
10
- * 'o365' — Microsoft Graph API (client credentials, no extra deps)
11
- * 'smtp' — nodemailer (optional peer dep: npm install nodemailer)
12
- * 'mock' — console.log only, safe for development/testing
13
- *
14
- * @param {object} config - merged plugin config
15
- * @returns {{ send: Function }}
16
- */
17
- function getProvider(config) {
18
- const providerName = (config.mail && config.mail.provider)
19
- ? String(config.mail.provider).toLowerCase()
20
- : 'mock';
21
-
22
- switch (providerName) {
23
- case 'o365':
24
- return require('./o365');
25
-
26
- case 'smtp':
27
- return require('./smtp');
28
-
29
- case 'mock':
30
- return require('./mock');
31
-
32
- default:
33
- console.warn(
34
- `[cds-error-outbox] Unknown email provider "${providerName}". Falling back to mock.`
35
- );
36
- return require('./mock');
37
- }
38
- }
39
-
40
- module.exports = { getProvider };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Email provider factory.
5
+ *
6
+ * Returns a provider object with a `send(mailConfig, subject, html)` function
7
+ * based on config.mail.provider. Falls back to 'mock' for unknown values.
8
+ *
9
+ * Supported providers:
10
+ * 'o365' — Microsoft Graph API (client credentials, no extra deps)
11
+ * 'smtp' — nodemailer (optional peer dep: npm install nodemailer)
12
+ * 'mock' — console.log only, safe for development/testing
13
+ *
14
+ * @param {object} config - merged plugin config
15
+ * @returns {{ send: Function }}
16
+ */
17
+ function getProvider(config) {
18
+ const providerName = (config.mail && config.mail.provider)
19
+ ? String(config.mail.provider).toLowerCase()
20
+ : 'mock';
21
+
22
+ switch (providerName) {
23
+ case 'o365':
24
+ return require('./o365');
25
+
26
+ case 'smtp':
27
+ return require('./smtp');
28
+
29
+ case 'mock':
30
+ return require('./mock');
31
+
32
+ default:
33
+ console.warn(
34
+ `[cds-error-outbox] Unknown email provider "${providerName}". Falling back to mock.`
35
+ );
36
+ return require('./mock');
37
+ }
38
+ }
39
+
40
+ module.exports = { getProvider };
package/providers/mock.js CHANGED
@@ -1,23 +1,23 @@
1
- 'use strict';
2
-
3
- /**
4
- * Mock email provider — logs to console only.
5
- * No external calls are made.
6
- *
7
- * Use this in development and test environments to verify the plugin
8
- * is capturing and formatting errors without needing real mail credentials.
9
- *
10
- * @param {object} mailConfig
11
- * @param {string} subject
12
- * @param {string} html
13
- */
14
- async function send(mailConfig, subject, html) {
15
- console.log('[cds-error-outbox][mock] ──────────── Email (mock) ────────────');
16
- console.log(`[cds-error-outbox][mock] From : ${mailConfig.from || '(not configured)'}`);
17
- console.log(`[cds-error-outbox][mock] To : ${mailConfig.to || '(not configured)'}`);
18
- console.log(`[cds-error-outbox][mock] Subject: ${subject}`);
19
- console.log(`[cds-error-outbox][mock] HTML : ${html ? html.length : 0} chars`);
20
- console.log('[cds-error-outbox][mock] ─────────────────────────────────────');
21
- }
22
-
23
- module.exports = { send };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Mock email provider — logs to console only.
5
+ * No external calls are made.
6
+ *
7
+ * Use this in development and test environments to verify the plugin
8
+ * is capturing and formatting errors without needing real mail credentials.
9
+ *
10
+ * @param {object} mailConfig
11
+ * @param {string} subject
12
+ * @param {string} html
13
+ */
14
+ async function send(mailConfig, subject, html) {
15
+ console.log('[cds-error-outbox][mock] ──────────── Email (mock) ────────────');
16
+ console.log(`[cds-error-outbox][mock] From : ${mailConfig.from || '(not configured)'}`);
17
+ console.log(`[cds-error-outbox][mock] To : ${mailConfig.to || '(not configured)'}`);
18
+ console.log(`[cds-error-outbox][mock] Subject: ${subject}`);
19
+ console.log(`[cds-error-outbox][mock] HTML : ${html ? html.length : 0} chars`);
20
+ console.log('[cds-error-outbox][mock] ─────────────────────────────────────');
21
+ }
22
+
23
+ module.exports = { send };