@stacksjs/notifications 0.75.126 → 0.75.128
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/drivers/database.d.ts +5 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -3
- package/package.json +11 -11
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare function notificationChannel(userId: number): string;
|
|
2
|
+
declare function publishNotification(userId: number, event: string, data: Record<string, unknown>): void;
|
|
3
|
+
declare function insertNotification(options: CreateNotificationOptions, uuid?: string): Promise<DatabaseNotification>;
|
|
1
4
|
export declare function useDatabase(): typeof DatabaseNotificationDriver;
|
|
2
5
|
/**
|
|
3
6
|
* @defaultValue
|
|
@@ -43,11 +46,6 @@ export declare interface CreateNotificationOptions {
|
|
|
43
46
|
userId: number
|
|
44
47
|
type: string
|
|
45
48
|
data: Record<string, unknown>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
declare interface InsertResultLike {
|
|
49
|
-
id?: number | bigint
|
|
50
|
-
insertId?: number | bigint
|
|
51
|
-
lastInsertRowid?: number | bigint
|
|
52
|
-
lastInsertId?: number | bigint
|
|
49
|
+
idempotencyKey?: string
|
|
50
|
+
broadcast?: boolean
|
|
53
51
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -102,6 +102,7 @@ export declare interface NotificationRecipient {
|
|
|
102
102
|
chatRecipient?: string | string[]
|
|
103
103
|
userId?: number
|
|
104
104
|
pushTokens?: string | string[]
|
|
105
|
+
pushDriver?: import('@stacksjs/push').PushDriver
|
|
105
106
|
broadcastChannel?: string
|
|
106
107
|
}
|
|
107
108
|
export declare interface NotifyResult {
|
|
@@ -118,7 +119,7 @@ export declare interface SmsTransport {
|
|
|
118
119
|
export type NotificationChannel = 'email' | 'sms' | 'chat' | 'database' | 'push' | 'broadcast';
|
|
119
120
|
export { BroadcastNotificationDriver } from './drivers/broadcast';
|
|
120
121
|
export { makeDeliveryRecord, recordNotificationDelivery, resolveDeliveryRecipient } from './delivery';
|
|
121
|
-
export { DatabaseNotificationDriver } from './drivers/database';
|
|
122
|
+
export { DatabaseNotificationDriver, notificationChannel } from './drivers/database';
|
|
122
123
|
export {
|
|
123
124
|
bulkSetPreferences,
|
|
124
125
|
filterChannelsByPreferences,
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{log as
|
|
2
|
+
import{log as _}from"@stacksjs/cli";import{notification as oe}from"@stacksjs/config";import{mail as U}from"@stacksjs/email";import{log as D}from"@stacksjs/cli";var d={async send(e){let t=e.event??"notification",r=e.channel;if(!r)r=e.userId!==void 0?`private-user.${e.userId}`:"notifications";try{let n=await import("@stacksjs/realtime").catch(()=>null),a=n?.emit,i=n?.getServer;if(!a||!i)return{delivered:!1,channel:r,event:t,reason:"@stacksjs/realtime is not available"};if(!i())return{delivered:!1,channel:r,event:t,reason:"realtime server not running"};let o=e.private??e.userId!==void 0;return a(r,t,e.data,{private:o}),D.info(`[notifications] broadcast to '${r}' (${t})`),{delivered:!0,channel:r,event:t}}catch(n){let a=n instanceof Error?n.message:String(n);return D.warn(`[notifications] broadcast failed: ${a}`),{delivered:!1,channel:r,event:t,reason:a}}}};import*as T from"@stacksjs/chat";import{log as S}from"@stacksjs/cli";import{createHash as Q}from"crypto";import{db as u,enqueueAfterCommit as z,lockRow as G,sqlDateTime as g}from"@stacksjs/database/runtime";import{emit as V,getServer as W}from"@stacksjs/realtime";function A(e){return`private-user.${e}`}function b(e,t,r){let n=()=>{try{if(W())V(A(e),t,r)}catch{S.warn("Notification inbox broadcast failed; the persisted inbox remains available")}};if(!z(n))n()}async function C(e,t){let r=g(),a=await u.insertInto("notifications").values({user_id:e.userId,...t?{uuid:t}:{},type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:r,updated_at:r}).returning("id").execute(),i=Array.isArray(a)?a[0]:a,o=Number(i?.id??i?.insertId??i?.lastInsertRowid??i?.lastInsertId??0);if(!Number.isSafeInteger(o)||o<=0)throw Error("Database notification did not return a generated id");if(S.info(`Database notification sent to user ${e.userId}: ${e.type}`),e.broadcast!==!1)b(e.userId,"notifications.created",{id:o});return{id:o,user_id:e.userId,type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:r,updated_at:r}}var m={async send(e){if(e.idempotencyKey!==void 0){if(!e.idempotencyKey||e.idempotencyKey.length>255)throw Error("A notification idempotency key must contain 1 to 255 characters");let t=Q("sha256").update(JSON.stringify([e.userId,e.type,e.idempotencyKey])).digest("hex"),r=`${t.slice(0,8)}-${t.slice(8,12)}-8${t.slice(13,16)}-8${t.slice(17,20)}-${t.slice(20,32)}`;return u.transaction(async(n)=>{if(!await G(n,"users",{id:e.userId}))throw Error("Notification recipient not found");let i=await u.selectFrom("notifications").selectAll().where("user_id","=",e.userId).where("uuid","=",r).executeTakeFirst();if(i)return{...i,id:Number(i.id),user_id:Number(i.user_id)};return C(e,r)})}return C(e)},async getUserNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).orderBy("created_at","desc").execute()).map((r)=>({...r,id:Number(r.id),user_id:Number(r.user_id)}))},async getUnreadNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).whereNull("read_at").orderBy("created_at","desc").execute()).map((r)=>({...r,id:Number(r.id),user_id:Number(r.user_id)}))},async markAsRead(e,t){let r=u.updateTable("notifications").set({read_at:g()}).where("id","=",e).whereNull("read_at");if(t!==void 0)r=r.where("user_id","=",t);if(await r.execute(),t)b(t,"notifications.read",{id:e})},async markAllAsRead(e){await u.updateTable("notifications").set({read_at:g()}).where("user_id","=",e).whereNull("read_at").execute(),b(e,"notifications.read",{all:!0})},async unreadCount(e){let t=await u.selectFrom("notifications").select(u.fn.countAll().as("count")).where("user_id","=",e).whereNull("read_at").executeTakeFirst();return Number(t?.count??0)},async deleteNotification(e,t){let r=u.deleteFrom("notifications").where("id","=",e);if(t!==void 0)r=r.where("user_id","=",t);await r.execute()},async deleteAllNotifications(e){await u.deleteFrom("notifications").where("user_id","=",e).execute()}};import*as I from"@stacksjs/push";import*as h from"@stacksjs/sms";import{log as X}from"@stacksjs/cli";import{config as j}from"@stacksjs/config";import{db as Y,sqlDateTime as B}from"@stacksjs/database/runtime";function O(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 Array.isArray(e.chatRecipient)?e.chatRecipient.join(", "):e.chatRecipient||""}}function y(e,t,r,n,a=B()){return{userId:e.userId,channel:r.channel,recipient:O(e,r.channel),subject:t.subject,body:t.body,status:r.success?"sent":"failed",error:r.error?.message,metadata:{...t.data,...n.category?{category:n.category}:{}},sentAt:a}}function Z(e){let t=j.notification?.tracking;if(t?.enabled===!1)return!1;let r=t?.channels;if(!r||r.length===0)return!0;return r.includes(e)}async function w(e){if(!Z(e.channel))return;let t=B();try{await Y.insertInto("notification_deliveries").values({user_id:e.userId??null,channel:e.channel,recipient:e.recipient,subject:e.subject??null,body:j.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(r){let n=r instanceof Error?r.message:String(r);X.warn(`[notifications] could not record ${e.channel} delivery: ${n}`)}}import{templateByName as ee}from"@stacksjs/email";var te=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function q(e){return String(e??"").replace(/\r\n?/g,`
|
|
3
3
|
`).split(/\n\s*\n/).map((t)=>t.trim()).filter(Boolean).map((t)=>{let r=[];return t.split(`
|
|
4
|
-
`).forEach((n,
|
|
4
|
+
`).forEach((n,a)=>{if(a>0)r.push({br:!0});let i=0;for(let o of n.matchAll(te)){if(o.index>i)r.push({text:n.slice(i,o.index)});r.push({url:o[0]}),i=o.index+o[0].length}if(i<n.length)r.push({text:n.slice(i)})}),r})}function N(e){return String(e??"").replace(/[&<>"']/g,(t)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t])}async function v(e){try{let{html:r}=await ee("notification",{subject:e.subject??"",variables:{subject:e.subject??"",paragraphs:q(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(r)return r}catch{}let t=e.action?.url?`<p><a href="${N(e.action.url)}">${N(e.action.label||e.action.url)}</a></p>`:"";return`<p>${N(e.body)}</p>${t}`}function x(e){return e.action?.url?`${e.body}
|
|
5
5
|
|
|
6
|
-
${e.action.label||"Open"}: ${e.action.url}`:e.body}import{log as
|
|
6
|
+
${e.action.label||"Open"}: ${e.action.url}`:e.body}import{log as re}from"@stacksjs/cli";import{db as l,lockRow as M,sqlDateTime as ne}from"@stacksjs/database/runtime";var p="notification_preferences";function R(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function P(e,t){let r=new Map;try{let a=await R(l.selectFrom(p).select(["channel","enabled","category"]).where("user_id","=",e),t??null).execute();for(let i of a)r.set(i.channel,i.enabled===!0||i.enabled===1)}catch(n){re.debug?.(`[notifications] preferences lookup failed (table missing?): ${n.message}`)}return r}async function ie(e,t,r,n){await l.transaction(async(a)=>{await M(a,"users",{id:e}),await F(e,t,r,n)})}async function F(e,t,r,n){if(!Number.isSafeInteger(e)||e<=0||typeof r!=="boolean")throw Error("A notification preference needs a valid user id and enabled boolean");let a=ne(),i=n??null,o=l.selectFrom(p).select(["id"]).where("user_id","=",e).where("channel","=",t),s=await R(o,i).executeTakeFirst();if(s?.id){await l.updateTable(p).set({enabled:r,updated_at:a}).where("id","=",s.id).execute();return}await l.insertInto(p).values({user_id:e,channel:t,enabled:r,category:i,created_at:a,updated_at:a}).execute()}async function ae(e,t){await l.transaction(async(r)=>{await M(r,"users",{id:e});for(let n of t)await F(e,n.channel,n.enabled,n.category)})}async function k(e,t,r){let n=await P(e,r);if(r!==void 0){let a=await P(e,void 0);for(let[i,o]of a)if(!o||!n.has(i))n.set(i,o)}return t.filter((a)=>{let i=n.get(a);return i===void 0?!0:i===!0})}var se=oe;function ce(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function Ue(e){if(!e.length)throw Error("No notification channels were delivered");let t=e.filter((r)=>!r.success);if(t.length){let r=t.map((n)=>n.error instanceof Error?n.error:Error(`Notification delivery failed through ${n.channel}`));if(r.length===1)throw r[0];throw AggregateError(r,r.map((n)=>n.message).join("; "))}}function E(e){if(!e.success)throw Error(e.error||e.message||"Notification provider rejected delivery")}function L(e){let t=e||"slack";if(!["slack","discord","teams"].includes(t))throw Error(`Unsupported chat driver: ${t}`);return T[t]}function H(e){if(e)try{return U.use(e)}catch(t){_.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return U}function K(e){if(!e)return h;if(e!=="twilio"&&e!=="vonage")throw Error(`Unsupported SMS driver: ${e}`);let t=e;return{send:async(r)=>(await h.init(),h.getDriver(t).send(r))}}function ue(){return I}function fe(){return m}function Le(){return d}function le(e,t){let r=e||se?.default||"email",n=t;switch(r){case"email":return H(n);case"chat":return L(n);case"sms":return K(n);case"database":return fe();default:throw Error(`Notification type "${r}" is not supported`)}}async function He(e,t,r=["email"],n={}){let a=[...new Set(r)];if(e.userId&&!n.ignorePreferences)try{a=await k(e.userId,a,n.category)}catch(s){_.warn(`[notify] preferences filter failed, sending all channels: ${s.message}`)}let o=(await Promise.allSettled(a.map(async(s)=>{switch(s){case"email":{if(!e.email)throw Error("[notify] email channel requires recipient.email");let f=await H().send({to:e.email,subject:t.subject??"",text:x(t),html:await v(t)});ce(f);break}case"sms":{if(!e.phone)throw Error("[notify] sms channel requires recipient.phone");if(n.ignorePreferences!==!0){let{isPhoneOptedOut:f}=await import("@stacksjs/sms");if(await f(e.phone))throw Error("[notify] sms recipient has opted out (STOP)")}let c=K();E(await c.send({to:e.phone,body:t.body}));break}case"chat":{if(!e.chatRecipient||Array.isArray(e.chatRecipient)&&!e.chatRecipient.length)throw Error("[notify] chat channel requires recipient.chatRecipient");let c=L();E(await c.send({to:e.chatRecipient,content:t.body,subject:t.subject,data:t.data}));break}case"database":{if(!e.userId)throw Error("[notify] database channel requires recipient.userId");await m.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");let c=ue();E(await c.send(e.pushTokens,{title:t.subject,body:t.body,data:t.data},{driver:e.pushDriver}));break}case"broadcast":{let c=await d.send({channel:e.broadcastChannel,userId:e.userId,event:t.subject??"notification",data:{body:t.body,...t.data}});if(!c.delivered)throw Error(c.reason||"Broadcast notification was not delivered");break}default:throw Error(`Unsupported notification channel: ${s}`)}}))).map((s,c)=>{let f=a[c];if(!f)throw Error(`Missing notification channel for result ${c}`);if(s.status==="rejected"){let J=s.reason instanceof Error?s.reason.message:String(s.reason);_.warn(`[notify] ${f} channel failed: ${J}`)}return{channel:f,success:s.status==="fulfilled",error:s.status==="rejected"?s.reason instanceof Error?s.reason:Error(String(s.reason)):void 0}});return await Promise.all(o.map((s)=>w(y(e,t,s,n)))),o}function Ke(){return le()}export{d as BroadcastNotificationDriver,m as DatabaseNotificationDriver,ae as bulkSetPreferences,q as emailParagraphs,ce as ensureSuccessfulEmailResult,Ue as ensureSuccessfulNotificationResults,k as filterChannelsByPreferences,P as getNotificationPreferences,y as makeDeliveryRecord,Ke as notification,A as notificationChannel,v as notificationEmailHtml,x as notificationEmailText,He as notify,w as recordNotificationDelivery,O as resolveDeliveryRecipient,ie as setNotificationPreference,Le as useBroadcast,L as useChat,fe as useDatabase,H as useEmail,le as useNotification,ue as usePush,K as useSMS,R 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.128",
|
|
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.128",
|
|
68
|
+
"@stacksjs/cli": "0.75.128",
|
|
69
|
+
"@stacksjs/config": "0.75.128",
|
|
70
|
+
"@stacksjs/database": "0.75.128",
|
|
71
|
+
"@stacksjs/email": "0.75.128",
|
|
72
|
+
"@stacksjs/push": "0.75.128",
|
|
73
|
+
"@stacksjs/realtime": "0.75.128",
|
|
74
|
+
"@stacksjs/sms": "0.75.128"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@stacksjs/error-handling": "0.75.
|
|
78
|
-
"@stacksjs/types": "0.75.
|
|
77
|
+
"@stacksjs/error-handling": "0.75.128",
|
|
78
|
+
"@stacksjs/types": "0.75.128",
|
|
79
79
|
"better-dx": "^0.2.24"
|
|
80
80
|
}
|
|
81
81
|
}
|