@stacksjs/notifications 0.75.108 → 0.75.109
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 +1 -0
- package/dist/index.js +3 -3
- package/dist/preferences.d.ts +3 -0
- package/package.json +11 -11
|
@@ -44,6 +44,7 @@ export declare interface CreateNotificationOptions {
|
|
|
44
44
|
}
|
|
45
45
|
/** Shape of a row-insert result across the supported drivers. */
|
|
46
46
|
declare interface InsertResultLike {
|
|
47
|
+
id?: number | bigint
|
|
47
48
|
insertId?: number | bigint
|
|
48
49
|
lastInsertRowid?: number | bigint
|
|
49
50
|
lastInsertId?: number | bigint
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{log as
|
|
2
|
+
import{log as k}from"@stacksjs/cli";import{notification as Y}from"@stacksjs/config";import{mail as j}from"@stacksjs/email";import{log as D}from"@stacksjs/cli";var d={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}),D.info(`[notifications] broadcast to '${n}' (${t})`),{delivered:!0,channel:n,event:t}}catch(r){let i=r instanceof Error?r.message:String(r);return D.warn(`[notifications] broadcast failed: ${i}`),{delivered:!1,channel:n,event:t,reason:i}}}};import*as R from"@stacksjs/chat";import{log as U}from"@stacksjs/cli";import{db as u,sqlDateTime as h}from"@stacksjs/database/runtime";var m={async send(e){let t=h(),r=await u.insertInto("notifications").values({user_id:e.userId,type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:t,updated_at:t}).returning("id").execute(),i=Array.isArray(r)?r[0]:r,a=Number(i?.id??i?.insertId??i?.lastInsertRowid??i?.lastInsertId??0);if(!Number.isSafeInteger(a)||a<=0)throw Error("Database notification did not return a generated id");return U.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 u.selectFrom("notifications").selectAll().where("user_id","=",e).orderBy("created_at","desc").execute()).map((n)=>({...n,id:Number(n.id),user_id:Number(n.user_id)}))},async getUnreadNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).whereNull("read_at").orderBy("created_at","desc").execute()).map((n)=>({...n,id:Number(n.id),user_id:Number(n.user_id)}))},async markAsRead(e){await u.updateTable("notifications").set({read_at:h()}).where("id","=",e).whereNull("read_at").execute()},async markAllAsRead(e){await u.updateTable("notifications").set({read_at:h()}).where("user_id","=",e).whereNull("read_at").execute()},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){await u.deleteFrom("notifications").where("id","=",e).execute()},async deleteAllNotifications(e){await u.deleteFrom("notifications").where("user_id","=",e).execute()}};import*as E from"@stacksjs/push";import*as _ from"@stacksjs/sms";import{log as H}from"@stacksjs/cli";import{config as T}from"@stacksjs/config";import{db as J,sqlDateTime as C}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=C()){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 Q(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(!Q(e.channel))return;let t=C();try{await J.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);H.warn(`[notifications] could not record ${e.channel} delivery: ${r}`)}}import{templateByName as z}from"@stacksjs/email";var G=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function S(e){return String(e??"").replace(/\r\n?/g,`
|
|
3
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(
|
|
4
|
+
`).forEach((r,i)=>{if(i>0)n.push({br:!0});let a=0;for(let s of r.matchAll(G)){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 z("notification",{subject:e.subject??"",variables:{subject:e.subject??"",paragraphs:S(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
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 K}from"@stacksjs/cli";import{db as l,lockRow as A,sqlDateTime as V}from"@stacksjs/database/runtime";var p="notification_preferences";function x(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function v(e,t){let n=new Map;try{let i=await x(l.selectFrom(p).select(["channel","enabled","category"]).where("user_id","=",e),t??null).execute();for(let a of i)n.set(a.channel,a.enabled===!0||a.enabled===1)}catch(r){K.debug?.(`[notifications] preferences lookup failed (table missing?): ${r.message}`)}return n}async function W(e,t,n,r){await l.transaction(async(i)=>{await A(i,"users",{id:e}),await B(e,t,n,r)})}async function B(e,t,n,r){if(!Number.isSafeInteger(e)||e<=0||typeof n!=="boolean")throw Error("A notification preference needs a valid user id and enabled boolean");let i=V(),a=r??null,s=l.selectFrom(p).select(["id"]).where("user_id","=",e).where("channel","=",t),o=await x(s,a).executeTakeFirst();if(o?.id){await l.updateTable(p).set({enabled:n,updated_at:i}).where("id","=",o.id).execute();return}await l.insertInto(p).values({user_id:e,channel:t,enabled:n,category:a,created_at:i,updated_at:i}).execute()}async function X(e,t){await l.transaction(async(n)=>{await A(n,"users",{id:e});for(let r of t)await B(e,r.channel,r.enabled,r.category)})}async function P(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(!s||!r.has(a))r.set(a,s)}return t.filter((i)=>{let a=r.get(i);return a===void 0?!0:a===!0})}var Z=Y;function ee(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function q(e){return R[e||"slack"]}function F(e){if(e)try{return j.use(e)}catch(t){k.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return j}function M(e){return _[e||"twilio"]}function te(){return E}function ne(){return m}function Ie(){return d}function re(e,t){let n=e||Z?.default||"email",r=t;switch(n){case"email":return F(r);case"chat":return q(r);case"sms":return M(r);case"database":return ne();default:throw Error(`Notification type "${n}" is not supported`)}}function O(e){return!!e&&typeof e==="object"&&"send"in e&&typeof e.send==="function"}async function Se(e,t,n=["email"],r={}){let i=n;if(e.userId&&!r.ignorePreferences)try{i=await P(e.userId,n,r.category)}catch(o){k.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 f=await F().send({to:e.email,subject:t.subject??"",text:N(t),html:await w(t)});ee(f);break}case"sms":{if(!e.phone)throw Error("[notify] sms channel requires recipient.phone");if(r.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=M();if(!O(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=q();if(!O(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 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");await te().send(e.pushTokens,{title:t.subject,body:t.body,data:t.data});break}case"broadcast":{await d.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 f=i[c];if(!f)throw Error(`Missing notification channel for result ${c}`);if(o.status==="rejected"){let L=o.reason instanceof Error?o.reason.message:String(o.reason);k.warn(`[notify] ${f} channel failed: ${L}`)}return{channel:f,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 Ae(){return re()}export{d as BroadcastNotificationDriver,m as DatabaseNotificationDriver,X as bulkSetPreferences,S as emailParagraphs,ee as ensureSuccessfulEmailResult,P as filterChannelsByPreferences,v as getNotificationPreferences,b as makeDeliveryRecord,Ae as notification,w as notificationEmailHtml,N as notificationEmailText,Se as notify,g as recordNotificationDelivery,I as resolveDeliveryRecipient,W as setNotificationPreference,Ie as useBroadcast,q as useChat,ne as useDatabase,F as useEmail,re as useNotification,te as usePush,M as useSMS,x as wherePreferenceCategory};
|
package/dist/preferences.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { NotificationChannel } from './index';
|
|
|
2
2
|
export declare function wherePreferenceCategory<T extends PreferenceCategoryQuery<T>>(query: T, category: string | null): T;
|
|
3
3
|
/**
|
|
4
4
|
* Load all notification preferences for a user as a Map keyed by channel.
|
|
5
|
+
* Without a category, returns global preferences only. Category-specific
|
|
6
|
+
* preferences are read only when that category is explicitly requested.
|
|
5
7
|
* Returns `Map<channel, enabled>` — channels with no row recorded are
|
|
6
8
|
* absent from the Map (callers should treat absent = enabled by default).
|
|
7
9
|
*
|
|
@@ -35,6 +37,7 @@ export declare function setNotificationPreference(userId: number, channel: Prefe
|
|
|
35
37
|
* Bulk upsert preferences for a user — convenient for "save preferences"
|
|
36
38
|
* forms that submit the user's full opt-in/out matrix in one POST.
|
|
37
39
|
*
|
|
40
|
+
* Saves the entire matrix inside one transaction and one user lock.
|
|
38
41
|
* Iterates one upsert per entry rather than one mega-query because the
|
|
39
42
|
* underlying conflict logic is keyed on `(user_id, channel, category)`
|
|
40
43
|
* and a single SQL statement can't express that across N rows portably.
|
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.109",
|
|
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.109",
|
|
68
|
+
"@stacksjs/cli": "0.75.109",
|
|
69
|
+
"@stacksjs/config": "0.75.109",
|
|
70
|
+
"@stacksjs/database": "0.75.109",
|
|
71
|
+
"@stacksjs/email": "0.75.109",
|
|
72
|
+
"@stacksjs/push": "0.75.109",
|
|
73
|
+
"@stacksjs/realtime": "0.75.109",
|
|
74
|
+
"@stacksjs/sms": "0.75.109"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@stacksjs/error-handling": "0.75.
|
|
78
|
-
"@stacksjs/types": "0.75.
|
|
77
|
+
"@stacksjs/error-handling": "0.75.109",
|
|
78
|
+
"@stacksjs/types": "0.75.109",
|
|
79
79
|
"better-dx": "^0.2.24"
|
|
80
80
|
}
|
|
81
81
|
}
|