@stacksjs/notifications 0.75.83 → 0.75.85
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/dist/email-body.d.ts +32 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/package.json +11 -11
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plain-text notification body as the paragraphs of its email. Blank lines
|
|
3
|
+
* separate paragraphs, a single newline stays a line break, and an address is
|
|
4
|
+
* a link. The template escapes every part, so a body is never HTML.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emailParagraphs(body: string): EmailBodyPart[][];
|
|
7
|
+
/**
|
|
8
|
+
* The HTML a notification is emailed as: the `notification` template (an
|
|
9
|
+
* app's own resources/emails/notification first, else the framework's), in
|
|
10
|
+
* the same layout as every other Stacks email. It used to be the body in a
|
|
11
|
+
* bare `<p>`, which mail clients showed as unstyled text with dead links.
|
|
12
|
+
* Falls back to that only if the template cannot be rendered.
|
|
13
|
+
*/
|
|
14
|
+
export declare function notificationEmailHtml(payload: { subject?: string, body: string, action?: NotificationAction }): Promise<string>;
|
|
15
|
+
/** The plain-text part, with the action's link spelled out. */
|
|
16
|
+
export declare function notificationEmailText(payload: { body: string, action?: NotificationAction }): string;
|
|
17
|
+
/** Where a notification leads: the email's button. */
|
|
18
|
+
export declare interface NotificationAction {
|
|
19
|
+
label: string
|
|
20
|
+
url: string
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One run of a notification email paragraph: text, a link, or a line break.
|
|
24
|
+
* A type, not an interface: template variables take index-signature types,
|
|
25
|
+
* which an interface does not satisfy.
|
|
26
|
+
*/
|
|
27
|
+
// eslint-disable-next-line ts/consistent-type-definitions
|
|
28
|
+
export type EmailBodyPart = {
|
|
29
|
+
text?: string
|
|
30
|
+
url?: string
|
|
31
|
+
br?: boolean
|
|
32
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,11 +3,13 @@ import { chat, email, push, sms } from './drivers/index';
|
|
|
3
3
|
import { DatabaseNotificationDriver } from './drivers/database';
|
|
4
4
|
import { filterChannelsByPreferences } from './preferences';
|
|
5
5
|
import { makeDeliveryRecord, recordNotificationDelivery } from './delivery';
|
|
6
|
+
import { type NotificationAction, notificationEmailHtml, notificationEmailText } from './email-body';
|
|
6
7
|
import type { EmailMessage, EmailResult } from '@stacksjs/types';
|
|
7
8
|
export type { BroadcastNotificationOptions, BroadcastNotificationResult } from './drivers/broadcast';
|
|
8
9
|
export type { NotificationDeliveryRecord } from './delivery';
|
|
9
10
|
export type { CreateNotificationOptions, DatabaseNotification } from './drivers/database';
|
|
10
11
|
export type { NotificationPreferenceRow, PreferenceChannel } from './preferences';
|
|
12
|
+
export type { EmailBodyPart, NotificationAction } from './email-body';
|
|
11
13
|
export declare function ensureSuccessfulEmailResult(result: EmailResult): void;
|
|
12
14
|
export declare function useChat(driver?: string): typeof chat[keyof typeof chat];
|
|
13
15
|
/**
|
|
@@ -63,6 +65,7 @@ export declare interface NotificationPayload {
|
|
|
63
65
|
subject?: string
|
|
64
66
|
body: string
|
|
65
67
|
data?: Record<string, unknown>
|
|
68
|
+
action?: NotificationAction
|
|
66
69
|
}
|
|
67
70
|
export declare interface NotificationRecipient {
|
|
68
71
|
email?: string
|
|
@@ -87,3 +90,4 @@ export {
|
|
|
87
90
|
setNotificationPreference,
|
|
88
91
|
wherePreferenceCategory,
|
|
89
92
|
} from './preferences';
|
|
93
|
+
export { emailParagraphs, notificationEmailHtml, notificationEmailText } from './email-body';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{log as
|
|
2
|
+
import{log as P}from"@stacksjs/cli";import{notification as W}from"@stacksjs/config";import{mail as B}from"@stacksjs/email";import{log as k}from"@stacksjs/cli";var l={async send(e){let t=e.event??"notification",n=e.channel;if(!n)n=e.userId!==void 0?`private-user.${e.userId}`:"notifications";try{let r=await import("@stacksjs/realtime").catch(()=>null),i=r?.emit,a=r?.getServer;if(!i||!a)return{delivered:!1,channel:n,event:t,reason:"@stacksjs/realtime is not available"};if(!a())return{delivered:!1,channel:n,event:t,reason:"realtime server not running"};let s=e.private??e.userId!==void 0;return i(n,t,e.data,{private:s}),k.info(`[notifications] broadcast to '${n}' (${t})`),{delivered:!0,channel:n,event:t}}catch(r){let i=r instanceof Error?r.message:String(r);return k.warn(`[notifications] broadcast failed: ${i}`),{delivered:!1,channel:n,event:t,reason:i}}}};import*as D from"@stacksjs/chat";import{log as L}from"@stacksjs/cli";import{db as f,sqlDateTime as h}from"@stacksjs/database/runtime";var d={async send(e){let t=h(),r=await f.insertInto("notifications").values({user_id:e.userId,type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:t,updated_at:t}).execute(),i=Array.isArray(r)?r[0]:r,a=Number(i?.insertId??i?.lastInsertRowid??i?.lastInsertId??0);return L.info(`Database notification sent to user ${e.userId}: ${e.type}`),{id:a,user_id:e.userId,type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:t,updated_at:t}},async getUserNotifications(e){return await f.selectFrom("notifications").selectAll().where("user_id","=",e).orderBy("created_at","desc").execute()},async getUnreadNotifications(e){return await f.selectFrom("notifications").selectAll().where("user_id","=",e).whereNull("read_at").orderBy("created_at","desc").execute()},async markAsRead(e){await f.updateTable("notifications").set({read_at:h()}).where("id","=",e).execute()},async markAllAsRead(e){await f.updateTable("notifications").set({read_at:h()}).where("user_id","=",e).whereNull("read_at").execute()},async unreadCount(e){let t=await f.selectFrom("notifications").select(f.fn.countAll().as("count")).where("user_id","=",e).whereNull("read_at").executeTakeFirst();return Number(t?.count??0)},async deleteNotification(e){await f.deleteFrom("notifications").where("id","=",e).execute()},async deleteAllNotifications(e){await f.deleteFrom("notifications").where("user_id","=",e).execute()}};import*as R from"@stacksjs/push";import*as E from"@stacksjs/sms";import{log as U}from"@stacksjs/cli";import{config as T}from"@stacksjs/config";import{db as H,sqlDateTime as _}from"@stacksjs/database/runtime";function I(e,t){switch(t){case"email":return e.email||"";case"sms":return e.phone||"";case"push":return Array.isArray(e.pushTokens)?e.pushTokens.join(", "):e.pushTokens||"";case"broadcast":return e.broadcastChannel||(e.userId?`private-user.${e.userId}`:"notifications");case"database":return e.userId?`User #${e.userId}`:"";case"chat":return"Configured chat destination"}}function b(e,t,n,r,i=_()){return{userId:e.userId,channel:n.channel,recipient:I(e,n.channel),subject:t.subject,body:t.body,status:n.success?"sent":"failed",error:n.error?.message,metadata:{...t.data,...r.category?{category:r.category}:{}},sentAt:i}}function J(e){let t=T.notification?.tracking;if(t?.enabled===!1)return!1;let n=t?.channels;if(!n||n.length===0)return!0;return n.includes(e)}async function g(e){if(!J(e.channel))return;let t=_();try{await H.insertInto("notification_deliveries").values({user_id:e.userId??null,channel:e.channel,recipient:e.recipient,subject:e.subject??null,body:T.notification?.tracking?.body===!1?"":e.body,status:e.status,error:e.error??null,metadata:e.metadata?JSON.stringify(e.metadata):null,sent_at:e.sentAt,created_at:t,updated_at:t}).execute()}catch(n){let r=n instanceof Error?n.message:String(n);U.warn(`[notifications] could not record ${e.channel} delivery: ${r}`)}}import{templateByName as Q}from"@stacksjs/email";var z=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function C(e){return String(e??"").replace(/\r\n?/g,`
|
|
3
|
+
`).split(/\n\s*\n/).map((t)=>t.trim()).filter(Boolean).map((t)=>{let n=[];return t.split(`
|
|
4
|
+
`).forEach((r,i)=>{if(i>0)n.push({br:!0});let a=0;for(let s of r.matchAll(z)){if(s.index>a)n.push({text:r.slice(a,s.index)});n.push({url:s[0]}),a=s.index+s[0].length}if(a<r.length)n.push({text:r.slice(a)})}),n})}function y(e){return String(e??"").replace(/[&<>"']/g,(t)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t])}async function w(e){try{let{html:n}=await Q("notification",{subject:e.subject??"",variables:{subject:e.subject??"",paragraphs:C(e.body),preheader:String(e.body??"").replace(/\s+/g," ").trim().slice(0,140),...e.action?.url?{actionUrl:e.action.url,actionLabel:e.action.label||"Open"}:{}}});if(n)return n}catch{}let t=e.action?.url?`<p><a href="${y(e.action.url)}">${y(e.action.label||e.action.url)}</a></p>`:"";return`<p>${y(e.body)}</p>${t}`}function N(e){return e.action?.url?`${e.body}
|
|
5
|
+
|
|
6
|
+
${e.action.label||"Open"}: ${e.action.url}`:e.body}import{log as G}from"@stacksjs/cli";import{db as m,sqlDateTime as K}from"@stacksjs/database/runtime";var p="notification_preferences";function A(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function v(e,t){let n=new Map;try{let r=m.selectFrom(p).select(["channel","enabled","category"]).where("user_id","=",e);if(t!==void 0)r=r.where("category","=",t);let i=await r.execute();for(let a of i)n.set(a.channel,a.enabled===!0||a.enabled===1)}catch(r){G.debug?.(`[notifications] preferences lookup failed (table missing?): ${r.message}`)}return n}async function S(e,t,n,r){let i=K(),a=r??null,s=m.selectFrom(p).select(["id"]).where("user_id","=",e).where("channel","=",t),o=await A(s,a).executeTakeFirst();if(o?.id){await m.updateTable(p).set({enabled:n,updated_at:i}).where("id","=",o.id).execute();return}await m.insertInto(p).values({user_id:e,channel:t,enabled:n,category:a,created_at:i,updated_at:i}).execute()}async function V(e,t){for(let n of t)await S(e,n.channel,n.enabled,n.category)}async function x(e,t,n){let r=await v(e,n);if(n!==void 0){let i=await v(e,void 0);for(let[a,s]of i)if(!r.has(a))r.set(a,s)}return t.filter((i)=>{let a=r.get(i);return a===void 0?!0:a===!0})}var X=W;function Y(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function O(e){return D[e||"slack"]}function F(e){if(e)try{return B.use(e)}catch(t){P.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return B}function q(e){return E[e||"twilio"]}function Z(){return R}function ee(){return d}function _e(){return l}function te(e,t){let n=e||X?.default||"email",r=t;switch(n){case"email":return F(r);case"chat":return O(r);case"sms":return q(r);case"database":return ee();default:throw Error(`Notification type "${n}" is not supported`)}}function j(e){return!!e&&typeof e==="object"&&"send"in e&&typeof e.send==="function"}async function Ie(e,t,n=["email"],r={}){let i=n;if(e.userId&&!r.ignorePreferences)try{i=await x(e.userId,n,r.category)}catch(o){P.warn(`[notify] preferences filter failed, sending all channels: ${o.message}`)}let s=(await Promise.allSettled(i.map(async(o)=>{switch(o){case"email":{if(!e.email)throw Error("[notify] email channel requires recipient.email");let u=await F().send({to:e.email,subject:t.subject??"",text:N(t),html:await w(t)});Y(u);break}case"sms":{if(!e.phone)throw Error("[notify] sms channel requires recipient.phone");if(r.ignorePreferences!==!0){let{isPhoneOptedOut:u}=await import("@stacksjs/sms");if(await u(e.phone))throw Error("[notify] sms recipient has opted out (STOP)")}let c=q();if(!j(c))throw Error("[notify] sms channel is not configured: no usable SMS driver with a send() method");await c.send({to:e.phone,body:t.body});break}case"chat":{let c=O();if(!j(c))throw Error("[notify] chat channel is not configured: no usable chat driver with a send() method");await c.send({body:t.body});break}case"database":{if(!e.userId)throw Error("[notify] database channel requires recipient.userId");await d.send({userId:e.userId,type:t.subject||"notification",data:{body:t.body,...t.data}});break}case"push":{if(!e.pushTokens||Array.isArray(e.pushTokens)&&e.pushTokens.length===0)throw Error("[notify] push channel requires recipient.pushTokens");await Z().send(e.pushTokens,{title:t.subject,body:t.body,data:t.data});break}case"broadcast":{await l.send({channel:e.broadcastChannel,userId:e.userId,event:t.subject??"notification",data:{body:t.body,...t.data}});break}default:throw Error(`Unsupported notification channel: ${o}`)}}))).map((o,c)=>{let u=i[c];if(!u)throw Error(`Missing notification channel for result ${c}`);if(o.status==="rejected"){let M=o.reason instanceof Error?o.reason.message:String(o.reason);P.warn(`[notify] ${u} channel failed: ${M}`)}return{channel:u,success:o.status==="fulfilled",error:o.status==="rejected"?o.reason:void 0}});return await Promise.all(s.map((o)=>g(b(e,t,o,r)))),s}function Ce(){return te()}export{l as BroadcastNotificationDriver,d as DatabaseNotificationDriver,V as bulkSetPreferences,C as emailParagraphs,Y as ensureSuccessfulEmailResult,x as filterChannelsByPreferences,v as getNotificationPreferences,b as makeDeliveryRecord,Ce as notification,w as notificationEmailHtml,N as notificationEmailText,Ie as notify,g as recordNotificationDelivery,I as resolveDeliveryRecipient,S as setNotificationPreference,_e as useBroadcast,O as useChat,ee as useDatabase,F as useEmail,te as useNotification,Z as usePush,q as useSMS,A as wherePreferenceCategory};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/notifications",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.85",
|
|
6
6
|
"description": "The Stacks notifications integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -64,18 +64,18 @@
|
|
|
64
64
|
"prepublishOnly": "bun run build"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@stacksjs/chat": "0.75.
|
|
68
|
-
"@stacksjs/cli": "0.75.
|
|
69
|
-
"@stacksjs/config": "0.75.
|
|
70
|
-
"@stacksjs/database": "0.75.
|
|
71
|
-
"@stacksjs/email": "0.75.
|
|
72
|
-
"@stacksjs/push": "0.75.
|
|
73
|
-
"@stacksjs/realtime": "0.75.
|
|
74
|
-
"@stacksjs/sms": "0.75.
|
|
67
|
+
"@stacksjs/chat": "0.75.85",
|
|
68
|
+
"@stacksjs/cli": "0.75.85",
|
|
69
|
+
"@stacksjs/config": "0.75.85",
|
|
70
|
+
"@stacksjs/database": "0.75.85",
|
|
71
|
+
"@stacksjs/email": "0.75.85",
|
|
72
|
+
"@stacksjs/push": "0.75.85",
|
|
73
|
+
"@stacksjs/realtime": "0.75.85",
|
|
74
|
+
"@stacksjs/sms": "0.75.85"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@stacksjs/error-handling": "0.75.
|
|
78
|
-
"@stacksjs/types": "0.75.
|
|
77
|
+
"@stacksjs/error-handling": "0.75.85",
|
|
78
|
+
"@stacksjs/types": "0.75.85",
|
|
79
79
|
"better-dx": "^0.2.24"
|
|
80
80
|
}
|
|
81
81
|
}
|