@node-in-layers/notifications 0.0.0-stage → 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.
Files changed (56) hide show
  1. package/README.md +605 -2
  2. package/backend/features.d.ts +5 -0
  3. package/backend/features.js +350 -0
  4. package/backend/features.js.map +1 -0
  5. package/backend/index.d.ts +6 -0
  6. package/backend/index.js +7 -0
  7. package/backend/index.js.map +1 -0
  8. package/backend/internal-libs.d.ts +63 -0
  9. package/backend/internal-libs.js +100 -0
  10. package/backend/internal-libs.js.map +1 -0
  11. package/backend/libs.d.ts +23 -0
  12. package/backend/libs.js +48 -0
  13. package/backend/libs.js.map +1 -0
  14. package/backend/services.d.ts +4 -0
  15. package/backend/services.js +57 -0
  16. package/backend/services.js.map +1 -0
  17. package/backend/types.d.ts +36 -0
  18. package/backend/types.js +2 -0
  19. package/backend/types.js.map +1 -0
  20. package/core/features.d.ts +4 -0
  21. package/core/features.js +6 -0
  22. package/core/features.js.map +1 -0
  23. package/core/index.d.ts +7 -0
  24. package/core/index.js +8 -0
  25. package/core/index.js.map +1 -0
  26. package/core/internal-libs.d.ts +5 -0
  27. package/core/internal-libs.js +34 -0
  28. package/core/internal-libs.js.map +1 -0
  29. package/core/libs.d.ts +28 -0
  30. package/core/libs.js +57 -0
  31. package/core/libs.js.map +1 -0
  32. package/core/models/NotificationConfigs.d.ts +13 -0
  33. package/core/models/NotificationConfigs.js +44 -0
  34. package/core/models/NotificationConfigs.js.map +1 -0
  35. package/core/models/NotificationTemplates.d.ts +11 -0
  36. package/core/models/NotificationTemplates.js +40 -0
  37. package/core/models/NotificationTemplates.js.map +1 -0
  38. package/core/models/Notifications.d.ts +49 -0
  39. package/core/models/Notifications.js +94 -0
  40. package/core/models/Notifications.js.map +1 -0
  41. package/core/models/index.d.ts +3 -0
  42. package/core/models/index.js +4 -0
  43. package/core/models/index.js.map +1 -0
  44. package/core/services.d.ts +4 -0
  45. package/core/services.js +6 -0
  46. package/core/services.js.map +1 -0
  47. package/core/types.d.ts +323 -0
  48. package/core/types.js +220 -0
  49. package/core/types.js.map +1 -0
  50. package/index.d.ts +4 -0
  51. package/index.js +5 -0
  52. package/index.js.map +1 -0
  53. package/package.json +99 -4
  54. package/types.d.ts +55 -0
  55. package/types.js +6 -0
  56. package/types.js.map +1 -0
package/README.md CHANGED
@@ -1,3 +1,606 @@
1
- # Temporary Holding Version
1
+ # Notifications - Node In Layers
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ![Unit Tests](https://github.com/node-in-layers/notifications/actions/workflows/ut.yml/badge.svg?branch=main)
4
+ [![Coverage Status](https://coveralls.io/repos/github/Node-In-Layers/notifications/badge.svg?branch=main)](https://coveralls.io/github/Node-In-Layers/notifications?branch=main)
5
+
6
+ <img src="./public/nil.png" width="160" height="150" />
7
+
8
+ Send notifications from Node in Layers systems.
9
+
10
+ This notification framework is modular and expandable. By default it comes with the ability to send emails, but by implementing the simple notification channel interface, anything can be used to send notifications.
11
+
12
+ At a high level this package can:
13
+
14
+ - Be used on front ends as well as backends
15
+ - Send system notifications
16
+ - Send notifications to users
17
+ - Support multi-channel delivery
18
+ - Support templated notification messages by message type and channel
19
+ - Create custom notifiers
20
+ - Be configured locally or via a database model
21
+ - Configured to utilize tasks/queue messaging bus
22
+
23
+ ## Out of the Box Notifiers
24
+
25
+ This package provides notifiers for:
26
+
27
+ - Email
28
+
29
+ ## Quick start
30
+
31
+ ### Install
32
+
33
+ ```bash
34
+ npm install @node-in-layers/notifications
35
+ ```
36
+
37
+ If you persist notifications, or you resolve users from `@node-in-layers/auth`, also install the packages those features use (`@node-in-layers/data`, `@node-in-layers/auth`).
38
+
39
+ ### Configure
40
+
41
+ Register both domains. Core comes before backend. For email, set `email`. For another channel only, set `notifiers` and leave `email` out. `email` is required only when `notifiers` is omitted.
42
+
43
+ ```typescript
44
+ import { CoreNamespace } from '@node-in-layers/core'
45
+ import {
46
+ notificationsCore,
47
+ NotificationsNamespace,
48
+ } from '@node-in-layers/notifications'
49
+ import * as notificationsBackend from '@node-in-layers/notifications/backend/index.js'
50
+
51
+ const config = {
52
+ [CoreNamespace.root]: {
53
+ domains: [
54
+ // node-in-layers/data goes here if using database models
55
+ notificationsCore, // core before backend
56
+ notificationsBackend,
57
+ // Insert user domains after
58
+ ],
59
+ },
60
+ [NotificationsNamespace.Backend]: {
61
+ adminEmails: ['ops@example.com'],
62
+ email: {
63
+ host: 'smtp.example.com',
64
+ port: 587,
65
+ secure: false,
66
+ auth: {
67
+ user: 'apikey',
68
+ pass: process.env.SMTP_PASSWORD,
69
+ },
70
+ from: 'notifications@example.com',
71
+ },
72
+ },
73
+ }
74
+ ```
75
+
76
+ `adminEmails` is who receives a system notification when the call does not pass its own recipients. `email.from` is the From address on every email this package sends.
77
+
78
+ ### Example: Send a system notification
79
+
80
+ ```typescript
81
+ import { NotificationsNamespace } from '@node-in-layers/notifications'
82
+
83
+ const response = await context.features[
84
+ NotificationsNamespace.Backend
85
+ ].sendSystemNotification(
86
+ {
87
+ title: 'Deploy finished',
88
+ message: 'The nightly import completed.',
89
+ severity: 'info',
90
+ },
91
+ crossLayerProps
92
+ )
93
+ ```
94
+
95
+ That email goes to `adminEmails`. The subject and body are rendered from the built-in system template. `data` on the call is available inside that template.
96
+
97
+ ### Example: Notify when a feature fails
98
+
99
+ Wrap the feature. If it throws, or if it returns a Node in Layers error object, a system notification is sent and the original failure is returned to the caller.
100
+
101
+ ```typescript
102
+ import { FeaturesContext, annotatedFunction } from '@node-in-layers/core'
103
+ import { NotificationsNamespace } from '@node-in-layers/notifications'
104
+ import { z } from 'zod'
105
+
106
+ export const create = (context: FeaturesContext<any, any>) => {
107
+ const notifications = context.features[NotificationsNamespace.Backend]
108
+
109
+ const createUser = notifications.wrapWithFailureNotification(
110
+ {
111
+ domain: 'users',
112
+ functionName: 'createUser',
113
+ },
114
+ annotatedFunction(
115
+ {
116
+ functionName: 'createUser',
117
+ domain: 'users',
118
+ description: 'Creates a user.',
119
+ args: z.object({
120
+ email: z.string(),
121
+ firstName: z.string(),
122
+ }),
123
+ returns: z.object({
124
+ id: z.string(),
125
+ }),
126
+ },
127
+ async (props, crossLayerProps) => {
128
+ const log = context.log.getInnerLogger('createUser', crossLayerProps)
129
+ log.info('Creating user', { email: props.email })
130
+
131
+ const created = await context.services.users.insertUser(
132
+ props,
133
+ crossLayerProps
134
+ )
135
+ return { id: created.id }
136
+ }
137
+ )
138
+ )
139
+
140
+ return {
141
+ createUser,
142
+ }
143
+ }
144
+ ```
145
+
146
+ The failure email includes the domain, function name, error, and the arguments that were passed in.
147
+
148
+ You can also send that notification yourself:
149
+
150
+ ```typescript
151
+ const created = await context.services.users
152
+ .insertUser(props, crossLayerProps)
153
+ .catch(async error => {
154
+ await context.features[
155
+ NotificationsNamespace.Backend
156
+ ].sendExecutionFailureNotification(
157
+ {
158
+ domain: 'users',
159
+ functionName: 'createUser',
160
+ error,
161
+ args: props,
162
+ },
163
+ crossLayerProps
164
+ )
165
+ throw error
166
+ })
167
+ ```
168
+
169
+ ### Example: Send a user notification
170
+
171
+ `sendUserNotification` emails the `user` you pass. That is the person this notification is for. `crossLayerProps.user` is the logged-in user and is not read.
172
+
173
+ ```typescript
174
+ await context.features[NotificationsNamespace.Backend].sendUserNotification(
175
+ {
176
+ user: approver,
177
+ title: 'Please review your account',
178
+ message: 'A change is waiting for your approval.',
179
+ data: {
180
+ reviewUrl: 'https://app.example.com/reviews/42',
181
+ },
182
+ },
183
+ crossLayerProps
184
+ )
185
+ ```
186
+
187
+ The template receives the user on `data.user`, so a custom template can say `Hello {{user.firstName}}`.
188
+
189
+ ## About This Package
190
+
191
+ ### Domains
192
+
193
+ #### NotificationNamespace.Core (@node-in-layers/notifications)
194
+
195
+ Contains all of the types, schemas and model definitions.
196
+
197
+ #### NotificationNamespace.Backend (@node-in-layers/notifications/backend)
198
+
199
+ Contains the backend implementation framework for notifications.
200
+
201
+ #### NotificationNamespace.Email (@node-in-layers/notifications/email)
202
+
203
+ Contains out of the box email implementation. Not required to be imported.
204
+
205
+ ## Features
206
+
207
+ These live on `context.features[NotificationNamespace.Backend`
208
+
209
+ | Feature | Type | What it does |
210
+ | ---------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
211
+ | `sendSystemNotification` | `system` | Sends an operational message. Recipients default to `adminEmails`. |
212
+ | `sendExecutionFailureNotification` | `executionFailure` | Sends a notification for a failed feature or service. Formats an `Error` or a Node in Layers error object into `data.error`. |
213
+ | `wrapWithFailureNotification` | `executionFailure` | Wraps a function. On throw or on a returned error object, sends `sendExecutionFailureNotification`, then preserves the original failure. |
214
+ | `sendUserNotification` | `user` | Sends to the `user` passed on the call. With email, that uses `user.email`. |
215
+ | `sendNotification` | caller sets `type` | Sends a notification of any type, including one you define. |
216
+
217
+ ### Notification type
218
+
219
+ Every notification has a `type`. The type chooses the template and decides what is in `data`.
220
+
221
+ The three built-in types are used by the provided features. Therefore you do not pass `type` to those features.
222
+
223
+ | Type | Set by | `data` contains |
224
+ | ------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
225
+ | `system` | `sendSystemNotification` | `systemName`, `environment`, `runtimeId`, `datetime`, `ids`, plus any `data` you passed. |
226
+ | `executionFailure` | `sendExecutionFailureNotification` | Everything on `system`, plus `domain`, `functionName`, `args`, and `error` (`code`, `message`, `details`, `trace`). |
227
+ | `user` | `sendUserNotification` | `user` (`id`, `email`, `firstName`, `lastName`, `username`), plus any `data` you passed. |
228
+
229
+ ### Sending Your Own Typed Notifications
230
+
231
+ `sendNotification` is how you send your own type. Pass `type` as a string, for example `accountReview`. Put the fields that template expects on `data`. A template for that type can use those fields directly: `{{reviewUrl}}`.
232
+
233
+ Your `data` is merged in. Fields the feature already set for that type stay as the feature set them.
234
+
235
+ ### Saved Notifications Model
236
+
237
+ When `persistNotifications` is true and model CRUDs are available, each send is stored on the `Notifications` model. Status moves from `pending` to `sent`, `partial_failure`, or `failed`. This makes it easy for an admin UI to list those rows from the core models. However, nothing is written when persistence is left off.
238
+
239
+ `NotificationConfigs` is optional. With `useDatabaseConfig: true`, a row whose `key` is `default` (or `databaseConfigKey`) overrides the static config: admin emails, from address, and the notifier map. Saved template bodies live on `NotificationTemplates`, not on that row.
240
+
241
+ A template is a Handlebars source string for one channel and one type. The same type can have a different string per channel. Dispatch resolves that string, and the notifier renders it into what that channel sends.
242
+
243
+ ## Core configuration
244
+
245
+ Put this on `[NotificationsNamespace.Core]`, which is `'@node-in-layers/notifications'`. It only affects stored notification rows.
246
+
247
+ | Field | Used for |
248
+ | ------------ | ---------------------------------------------------------------------------------------- |
249
+ | `defaultTtl` | Optional. Seconds from creation until a stored notification's `ttl`. Default is 90 days. |
250
+ | `noTTL` | When `true`, stored notifications have no `ttl`. |
251
+
252
+ A single send can pass `ttl`, in seconds, to override `defaultTtl` for that notification. `ttl` is written only when `persistNotifications` is true.
253
+
254
+ ## Backend configuration
255
+
256
+ Put this on `[NotificationsNamespace.Backend]`, which is `'@node-in-layers/notifications/backend'`.
257
+
258
+ `email` is required only when `notifiers` is omitted. If `notifiers` is set, those channels are used and SMTP is not required.
259
+
260
+ ### `email`
261
+
262
+ SMTP settings for the built-in email notifier. Omit this block when every channel you use is one of your own notifiers. When this block is set and `notifiers` is omitted, notifications go out through email.
263
+
264
+ | Field | Used for |
265
+ | ------------------ | --------------------------------------------------------------------------------- |
266
+ | `host` | SMTP hostname. |
267
+ | `port` | SMTP port. |
268
+ | `secure` | `true` for implicit TLS (typically port 465). Leave `false` for STARTTLS on 587. |
269
+ | `auth.user` | SMTP username. |
270
+ | `auth.pass` | SMTP password or API key. Read this from the environment. |
271
+ | `from` | From address on every email. |
272
+ | `defaultTo` | Recipients used when a notification has no recipients and `adminEmails` is empty. |
273
+ | `subjectPrefix` | Text prepended to every subject, for example `[my-app]`. |
274
+ | `transportOptions` | Extra options passed through to Nodemailer. |
275
+
276
+ ### Who gets system mail
277
+
278
+ | Field | Used for |
279
+ | ------------- | -------------------------------------------------------------------------------- |
280
+ | `adminEmails` | Default recipients for system notifications and execution-failure notifications. |
281
+
282
+ ### `templates`
283
+
284
+ `templates` maps a channel and a type to a Handlebars source string. A missing pair uses `message` as the body. [Templates](#templates) covers how that string is written, bundled, saved, and which variables it receives.
285
+
286
+ ### Channels
287
+
288
+ | Field | Used for |
289
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
290
+ | `notifiers` | Map of channel name to a `domain.feature` string. Omit this to use the built-in email notifier. If you set it, that map is what dispatch uses, and `email` is not required. Include the `email` key only when you still want the built-in mailer. |
291
+ | `defaultChannels` | Channels used when a call does not pass `channels` or `targets`. Omit it to use every key in the notifier map. With `notifiers` omitted and `email` set, that is `email`. |
292
+
293
+ ```typescript
294
+ [NotificationsNamespace.Backend]: {
295
+ email: {
296
+ host: 'smtp.example.com',
297
+ port: 587,
298
+ from: 'notifications@example.com',
299
+ },
300
+ notifiers: {
301
+ email: '@node-in-layers/notifications/backend.sendEmailNotification',
302
+ slack: 'slack.sendSlackNotification',
303
+ },
304
+ defaultChannels: ['email'],
305
+ }
306
+ ```
307
+
308
+ ### Users
309
+
310
+ `sendUserNotification` uses the `user` argument. Pass the person to notify. When the call has `userId` and no `user`, that id is loaded from the user model.
311
+
312
+ | Field | Used for |
313
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | `userResolution.userModel` | `domain.PluralModelName` of the user model. Default is `@node-in-layers/auth/core.Users`, or `userModel` from the auth core config when that is set. |
315
+ | `userResolution.userResolverFeature` | `domain.feature` that returns a user for a `userId`, when you do not want a model retrieve. |
316
+ | `userResolution.defaultChannels` | Channels used for user notifications when the call does not pass `channels`. When omitted, user notifications use the same default channels as other sends. Email uses `user.email`. A channel such as `sms` uses `user.phoneNumber`. |
317
+
318
+ A disabled user (`enabled: false`) is not notified.
319
+
320
+ ### Persistence and database config
321
+
322
+ | Field | Used for |
323
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
324
+ | `persistNotifications` | When `true`, each notification is saved with the core `Notifications` model. Requires model CRUDs. Default is off, so a system with only SMTP still sends mail. |
325
+ | `useDatabaseConfig` | When `true`, loads one `NotificationConfigs` row and overlays it on this config. |
326
+ | `databaseConfigKey` | Key of that row. Default is `default`. |
327
+ | `useDatabaseTemplates` | When `true`, loads a `NotificationTemplates` row for the channel and type being sent. A row replaces the config string for that pair. |
328
+
329
+ The `NotificationConfigs` row can set `adminEmails`, `fromEmail`, `subjectPrefix`, and `notifiers`. Values on the row replace the matching static values. Template bodies are not stored on that row.
330
+
331
+ ### `tasks`
332
+
333
+ `tasks` is optional. Leave it unset to send during the feature call.
334
+
335
+ | Field | Used for |
336
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
+ | `tasks.enabled` | When `true`, a send is placed on the `@node-in-layers/tasks` queue instead of being delivered during the call. That backend must be loaded. When omitted or `false`, the notification is sent during the feature call. |
338
+
339
+ ```typescript
340
+ [NotificationsNamespace.Backend]: {
341
+ tasks: {
342
+ enabled: true,
343
+ },
344
+ }
345
+ ```
346
+
347
+ ## Templates
348
+
349
+ `templates[channel][type]` is a Handlebars source string. It is not a file path. `Handlebars.compile` turns that string into a function at render time. You do not put that function, or a precompiled template object, in config. A database row stores the same source string.
350
+
351
+ ```typescript
352
+ templates: {
353
+ email: {
354
+ executionFailure: executionFailureEmail,
355
+ },
356
+ sms: {
357
+ executionFailure: executionFailureSms,
358
+ },
359
+ }
360
+ ```
361
+
362
+ A string on an email type is the plain-text body. `title` is the subject. When that email also needs HTML, set an object of source strings. `text` and `html` are each optional.
363
+
364
+ ```typescript
365
+ templates: {
366
+ email: {
367
+ executionFailure: {
368
+ text: executionFailureEmailText,
369
+ html: executionFailureEmailHtml,
370
+ },
371
+ },
372
+ }
373
+ ```
374
+
375
+ Other channels stay a single string. That string is the whole payload.
376
+
377
+ Leave a channel and type unset and that channel uses `message` as the body.
378
+
379
+ ### Bundling templates with esbuild
380
+
381
+ If you are going to esbuild bundle your system note that by default handlebar template paths will not be included. However, there are two solutions available.
382
+
383
+ 1. You can put the templates as strings inside of a typescript/javascript file.
384
+ 2. You can import ".hbs" files directly in with Node 26 or newer.
385
+
386
+ #### Importing HBS Files Directly
387
+
388
+ When the app is built with esbuild, import each template at the top of the module that builds this config, and put that binding in `templates`. Do not put a file path there.
389
+
390
+ ```typescript
391
+ import executionFailureEmail from './templates/email/execution-failure.hbs' with { type: 'text' }
392
+ import executionFailureSms from './templates/sms/execution-failure.hbs' with { type: 'text' }
393
+ ```
394
+
395
+ When running esbuild with bundle, esbuild reads the `.hbs` file during the build and writes the source into the output as a string. The file is not read again when the process starts, and it does not have to be deployed next to the bundle.
396
+
397
+ ```bash
398
+ esbuild src/config.mjs --bundle --outfile=dist/config.js
399
+ ```
400
+
401
+ ```javascript
402
+ var executionFailureEmail = '<p>{{domain}}.{{functionName}} failed</p>\n'
403
+ ```
404
+
405
+ `--bundle` is required for that. Without it, esbuild reprints the import and the template stays out of the output.
406
+
407
+ `tsc` does not know a `.hbs` import. Add this declaration so typechecking accepts it:
408
+
409
+ ```typescript
410
+ declare module '*.hbs' {
411
+ const source: string
412
+ export default source
413
+ }
414
+ ```
415
+
416
+ ### Saved templates
417
+
418
+ `NotificationTemplates` stores one template per channel and type. Turn this on with `useDatabaseTemplates: true`. Model CRUDs must be available. This is separate from `persistNotifications`: you can save templates without saving each send.
419
+
420
+ | Field | Used for |
421
+ | ---------- | ------------------------------------------------------------------------ |
422
+ | `channel` | Channel key, such as `email` or `sms`. |
423
+ | `type` | Notification type, such as `executionFailure` or `accountReview`. |
424
+ | `body` | Handlebars source. This becomes `template` on the notifier. |
425
+ | `htmlBody` | Optional HTML source for the email channel. This becomes `htmlTemplate`. |
426
+
427
+ Dispatch looks up the row for the channel and type on that send. A row replaces `templates[channel][type]` from config. No row means the config string is used. Neither one means the notifier receives no template and sends `message`.
428
+
429
+ These variables are already in every template. You do not put them on `data`.
430
+
431
+ | Variable | Value |
432
+ | ------------- | ---------------------------------------------------------- |
433
+ | `title` | Notification title. |
434
+ | `message` | Notification message. |
435
+ | `severity` | `info`, `warning`, `error`, or `critical`. |
436
+ | `type` | Notification type, such as `system` or `user`. |
437
+ | `channel` | Channel rendering this template, such as `email` or `sms`. |
438
+ | `systemName` | `systemName` from config. |
439
+ | `environment` | Current environment. |
440
+ | `runtimeId` | Runtime id for this process. |
441
+ | `datetime` | Time of the send. |
442
+
443
+ `executionFailure` also has these:
444
+
445
+ | Variable | Value |
446
+ | --------------- | ------------------------------------- |
447
+ | `domain` | Domain whose function failed. |
448
+ | `functionName` | Function that failed. |
449
+ | `args` | Arguments passed to that function. |
450
+ | `error.code` | Error code, when the failure has one. |
451
+ | `error.message` | Error message. |
452
+ | `error.details` | Extra error text, when present. |
453
+ | `error.trace` | Stack trace, when present. |
454
+
455
+ `user` also has `user.id`, `user.email`, `user.firstName`, `user.lastName`, `user.username`, and `user.phoneNumber`.
456
+
457
+ Keys you pass on `data` are available by name next to these. `data.reviewUrl` is `{{reviewUrl}}`. A key you pass does not replace a variable the feature already set for that type.
458
+
459
+ ## Create your own notifier
460
+
461
+ A notifier is the feature for one channel. Dispatch looks up `notifiers[channel]`, splits the `domain.feature` string, and calls that feature once per notification. The feature turns the notification into whatever that channel sends. Email renders a subject and a body. SMS renders one short string. Your feature does the same for its own channel.
462
+
463
+ Put the contract next to your feature. `createNotificationChannelSchema` is exported from `@node-in-layers/notifications`.
464
+
465
+ The first type argument is an optional shape of `notification.data`. That is the data for the notification type this channel handles. A channel that handles more than one type uses a union. The second type argument is the shape of `options`. Leave it off when the notifier does not read `options`.
466
+
467
+ ```typescript
468
+ import { annotatedFunction } from '@node-in-layers/core'
469
+ import {
470
+ createNotificationChannelSchema,
471
+ renderNotificationTemplate,
472
+ } from '@node-in-layers/notifications'
473
+
474
+ type AccountReviewData = Readonly<{
475
+ reviewUrl: string
476
+ }>
477
+
478
+ type SlackOptions = Readonly<{
479
+ username?: string
480
+ }>
481
+
482
+ const SendSlackNotificationSchema = createNotificationChannelSchema<
483
+ AccountReviewData,
484
+ SlackOptions
485
+ >({
486
+ functionName: 'sendSlackNotification',
487
+ domain: 'slack',
488
+ description: 'Posts a notification into Slack.',
489
+ })
490
+
491
+ export const create = context => {
492
+ const sendSlackNotification = annotatedFunction(
493
+ SendSlackNotificationSchema,
494
+ async (props, crossLayerProps) => {
495
+ const text = props.template
496
+ ? renderNotificationTemplate({
497
+ template: props.template,
498
+ notification: props.notification,
499
+ channel: props.channel,
500
+ })
501
+ : props.notification.message
502
+
503
+ await context.services.slack.postMessage(
504
+ {
505
+ channel: props.target?.channelId,
506
+ text,
507
+ username: props.options?.username,
508
+ },
509
+ crossLayerProps
510
+ )
511
+
512
+ return {
513
+ channel: props.channel,
514
+ notifier: 'slack.sendSlackNotification',
515
+ success: true,
516
+ }
517
+ }
518
+ )
519
+
520
+ return {
521
+ sendSlackNotification,
522
+ }
523
+ }
524
+ ```
525
+
526
+ The feature receives:
527
+
528
+ | Field | Meaning |
529
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
530
+ | `channel` | The key from `notifiers`, such as `slack` or `email`. |
531
+ | `notification` | The notification: `type`, `title`, `message`, `data`, recipients, and the rest of the stored record. |
532
+ | `target` | Present when the caller passed a target for this channel (`to`, `phoneNumbers`, `url`, `channelId`, and so on). |
533
+ | `template` | Handlebars source for this channel and `notification.type`. Omitted when neither config nor a saved row has one. |
534
+ | `htmlTemplate` | HTML source for the email channel, when the template was an object with `html` or the saved row has `htmlBody`. |
535
+ | `options` | Optional object from the send. The second type argument of `createNotificationChannelSchema` sets its keys. Omitted when the call did not pass `options`. |
536
+
537
+ `renderNotificationTemplate` compiles `template` and renders it with the [standard template variables](#templates). The notifier decides what to do with the string. Slack posts it. The built-in email notifier uses `title` as the subject, `template` as the plain-text body, and `htmlTemplate` as the HTML body when it is present.
538
+
539
+ Return `{ channel, notifier, success: true }` when the send worked. Return a Node in Layers error object when it did not. Dispatch records each result on the notification.
540
+
541
+ Point the channel at your feature. This setup does not use email:
542
+
543
+ ```typescript
544
+ [NotificationsNamespace.Backend]: {
545
+ notifiers: {
546
+ slack: 'slack.sendSlackNotification',
547
+ },
548
+ }
549
+ ```
550
+
551
+ Include `email` in that map, and set the `email` config, only when that channel should also send mail.
552
+
553
+ Send on that channel by name:
554
+
555
+ ```typescript
556
+ await context.features[NotificationsNamespace.Backend].sendNotification(
557
+ {
558
+ title: 'Build failed',
559
+ message: 'main is red.',
560
+ channels: ['slack'],
561
+ options: {
562
+ username: 'deploy-bot',
563
+ },
564
+ targets: [
565
+ {
566
+ channel: 'slack',
567
+ channelId: 'deploys',
568
+ },
569
+ ],
570
+ },
571
+ crossLayerProps
572
+ )
573
+ ```
574
+
575
+ A single call can include both `email` and `slack`. Each channel runs its own notifier.
576
+
577
+ ## Primary key type
578
+
579
+ Notification models do not choose an id type. Each model takes its primary key from `getPrimaryKeyProperty` in `@node-in-layers/core`, the same way other Node in Layers packages do (Example: `@node-in-layers/auth`). The default is `PropertyType.UniqueId`, a string UUID.
580
+
581
+ A database that uses integers sets that type on the core config. It applies to every model in the system, including notifications.
582
+
583
+ ```typescript
584
+ import { CoreNamespace } from '@node-in-layers/core'
585
+ import { PropertyType } from 'functional-models'
586
+
587
+ const config = {
588
+ [CoreNamespace.root]: {
589
+ modelIdPropertyType: PropertyType.Integer,
590
+ },
591
+ }
592
+ ```
593
+
594
+ To change only these models, set `modelNameToIdPropertyType`. The key is `domain/PluralModelName`.
595
+
596
+ ```typescript
597
+ [CoreNamespace.root]: {
598
+ modelNameToIdPropertyType: {
599
+ '@node-in-layers/notifications/Notifications': PropertyType.Integer,
600
+ '@node-in-layers/notifications/NotificationConfigs': PropertyType.Integer,
601
+ '@node-in-layers/notifications/NotificationTemplates': PropertyType.Integer,
602
+ },
603
+ }
604
+ ```
605
+
606
+ For integer keys, take the id from the database rather than generating one in the process. Set `primaryKeyGenerator` on the same core config when the database should supply it.
@@ -0,0 +1,5 @@
1
+ import { FeaturesContext } from '@node-in-layers/core';
2
+ import { ConfigWithNotifications } from '../types.js';
3
+ import { CoreServicesLayer } from '../core/types.js';
4
+ import { BackendFeatures, BackendFeaturesLayer, BackendServicesLayer } from './types.js';
5
+ export declare const create: (context: FeaturesContext<ConfigWithNotifications, BackendServicesLayer & CoreServicesLayer, BackendFeaturesLayer>) => BackendFeatures;