@stacksjs/notifications 0.70.100 → 0.70.102

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/index.d.ts CHANGED
@@ -81,4 +81,5 @@ export {
81
81
  filterChannelsByPreferences,
82
82
  getNotificationPreferences,
83
83
  setNotificationPreference,
84
+ wherePreferenceCategory,
84
85
  } from './preferences';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var P=import.meta.require;import{log as _}from"@stacksjs/cli";import{notification as k}from"@stacksjs/config";import{mail as T}from"@stacksjs/email";import{log as q}from"@stacksjs/cli";var V={async send(z){let F=z.event??"notification",J=z.channel;if(!J)J=z.userId!==void 0?`private-user-${z.userId}`:"notifications";try{let G=await import("@stacksjs/realtime").catch(()=>null),K=G?.emit,O=G?.getServer;if(!K||!O)return{delivered:!1,channel:J,event:F,reason:"@stacksjs/realtime is not available"};if(!O())return{delivered:!1,channel:J,event:F,reason:"realtime server not running"};let Q=z.private??z.userId!==void 0;return K(J,F,z.data,{private:Q}),q.info(`[notifications] broadcast to '${J}' (${F})`),{delivered:!0,channel:J,event:F}}catch(G){let K=G instanceof Error?G.message:String(G);return q.warn(`[notifications] broadcast failed: ${K}`),{delivered:!1,channel:J,event:F,reason:K}}}};import*as x from"@stacksjs/chat";import{log as D}from"@stacksjs/cli";import{db as U}from"@stacksjs/database";var W={async send(z){let F=new Date().toISOString(),G=await U.insertInto("notifications").values({user_id:z.userId,type:z.type,data:JSON.stringify(z.data),read_at:null,created_at:F,updated_at:F}).execute(),K=Array.isArray(G)?G[0]:G,O=Number(K?.insertId??K?.lastInsertRowid??K?.lastInsertId??0);return D.info(`Database notification sent to user ${z.userId}: ${z.type}`),{id:O,user_id:z.userId,type:z.type,data:JSON.stringify(z.data),read_at:null,created_at:F,updated_at:F}},async getUserNotifications(z){return await U.selectFrom("notifications").selectAll().where("user_id","=",z).orderBy("created_at","desc").execute()},async getUnreadNotifications(z){return await U.selectFrom("notifications").selectAll().where("user_id","=",z).where("read_at","is",null).orderBy("created_at","desc").execute()},async markAsRead(z){await U.updateTable("notifications").set({read_at:new Date().toISOString()}).where("id","=",z).execute()},async markAllAsRead(z){await U.updateTable("notifications").set({read_at:new Date().toISOString()}).where("user_id","=",z).where("read_at","is",null).execute()},async unreadCount(z){let F=await U.selectFrom("notifications").select(U.fn.countAll().as("count")).where("user_id","=",z).where("read_at","is",null).executeTakeFirst();return Number(F?.count??0)},async deleteNotification(z){await U.deleteFrom("notifications").where("id","=",z).execute()},async deleteAllNotifications(z){await U.deleteFrom("notifications").where("user_id","=",z).execute()}};import*as M from"@stacksjs/push";import*as A from"@stacksjs/sms";import{log as S}from"@stacksjs/cli";import{db as X}from"@stacksjs/database";var Y="notification_preferences";async function $(z,F){let J=new Map;try{let G=X.selectFrom(Y).select(["channel","enabled","category"]).where("user_id","=",z);if(F!==void 0)G=G.where("category","=",F);let K=await G.execute();for(let O of K)J.set(O.channel,O.enabled===!0||O.enabled===1)}catch(G){S.debug?.(`[notifications] preferences lookup failed (table missing?): ${G.message}`)}return J}async function L(z,F,J,G){let K=new Date().toISOString(),O=G??null,Q=await X.selectFrom(Y).select(["id"]).where("user_id","=",z).where("channel","=",F).where("category",O===null?"is":"=",O).executeTakeFirst();if(Q?.id){await X.updateTable(Y).set({enabled:J,updated_at:K}).where("id","=",Q.id).execute();return}await X.insertInto(Y).values({user_id:z,channel:F,enabled:J,category:O,created_at:K,updated_at:K}).execute()}async function w(z,F){for(let J of F)await L(z,J.channel,J.enabled,J.category)}async function H(z,F,J){let G=await $(z,J);if(J!==void 0){let K=await $(z,void 0);for(let[O,Q]of K)if(!G.has(O))G.set(O,Q)}return F.filter((K)=>{let O=G.get(K);return O===void 0?!0:O===!0})}var m=k;function E(z){return x[z||"slack"]}function B(z){if(z)try{return T.use(z)}catch(F){_.warn(`[notifications] email driver '${z}' not registered \u2014 falling back to default mail singleton (${F.message})`)}return T}function j(z){return A[z||"twilio"]}function I(){return M}function g(){return W}function Kz(){return V}function b(z,F){let J=z||m?.default||"email",G=F;switch(J){case"email":return B(G);case"chat":return E(G);case"sms":return j(G);case"database":return g();default:throw Error(`Notification type "${J}" is not supported`)}}function C(z){return!!z&&typeof z==="object"&&"send"in z&&typeof z.send==="function"}async function Oz(z,F,J=["email"],G={}){let K=J;if(z.userId&&!G.ignorePreferences)try{K=await H(z.userId,J,G.category)}catch(Q){_.warn(`[notify] preferences filter failed, sending all channels: ${Q.message}`)}return(await Promise.allSettled(K.map(async(Q)=>{switch(Q){case"email":{if(!z.email)throw Error("[notify] email channel requires recipient.email");await B().send({to:z.email,subject:F.subject??"",text:F.body,html:`<p>${f(F.body)}</p>`});break}case"sms":{if(!z.phone)throw Error("[notify] sms channel requires recipient.phone");let R=j();if(!C(R))throw Error("[notify] sms channel is not configured: no usable SMS driver with a send() method");await R.send({to:z.phone,body:F.body});break}case"chat":{let R=E();if(!C(R))throw Error("[notify] chat channel is not configured: no usable chat driver with a send() method");await R.send({body:F.body});break}case"database":{if(!z.userId)throw Error("[notify] database channel requires recipient.userId");await W.send({userId:z.userId,type:F.subject||"notification",data:{body:F.body,...F.data}});break}case"push":{if(!z.pushTokens||Array.isArray(z.pushTokens)&&z.pushTokens.length===0)throw Error("[notify] push channel requires recipient.pushTokens");await I().send(z.pushTokens,{title:F.subject,body:F.body,data:F.data});break}case"broadcast":{await V.send({channel:z.broadcastChannel,userId:z.userId,event:F.subject??"notification",data:{body:F.body,...F.data}});break}default:throw Error(`Unsupported notification channel: ${Q}`)}}))).map((Q,R)=>{let Z=K[R];if(!Z)throw Error(`Missing notification channel for result ${R}`);if(Q.status==="rejected"){let N=Q.reason instanceof Error?Q.reason.message:String(Q.reason);_.warn(`[notify] ${Z} channel failed: ${N}`)}return{channel:Z,success:Q.status==="fulfilled",error:Q.status==="rejected"?Q.reason:void 0}})}function Qz(){return b()}function f(z){return String(z??"").replace(/[&<>"']/g,(F)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[F])}export{j as useSMS,I as usePush,b as useNotification,B as useEmail,g as useDatabase,E as useChat,Kz as useBroadcast,L as setNotificationPreference,Oz as notify,Qz as notification,$ as getNotificationPreferences,H as filterChannelsByPreferences,w as bulkSetPreferences,W as DatabaseNotificationDriver,V as BroadcastNotificationDriver};
2
+ var P=import.meta.require;import{log as _}from"@stacksjs/cli";import{notification as m}from"@stacksjs/config";import{mail as q}from"@stacksjs/email";import{log as x}from"@stacksjs/cli";var W={async send(z){let F=z.event??"notification",J=z.channel;if(!J)J=z.userId!==void 0?`private-user-${z.userId}`:"notifications";try{let G=await import("@stacksjs/realtime").catch(()=>null),K=G?.emit,O=G?.getServer;if(!K||!O)return{delivered:!1,channel:J,event:F,reason:"@stacksjs/realtime is not available"};if(!O())return{delivered:!1,channel:J,event:F,reason:"realtime server not running"};let R=z.private??z.userId!==void 0;return K(J,F,z.data,{private:R}),x.info(`[notifications] broadcast to '${J}' (${F})`),{delivered:!0,channel:J,event:F}}catch(G){let K=G instanceof Error?G.message:String(G);return x.warn(`[notifications] broadcast failed: ${K}`),{delivered:!1,channel:J,event:F,reason:K}}}};import*as M from"@stacksjs/chat";import{log as S}from"@stacksjs/cli";import{db as V}from"@stacksjs/database";var X={async send(z){let F=new Date().toISOString(),G=await V.insertInto("notifications").values({user_id:z.userId,type:z.type,data:JSON.stringify(z.data),read_at:null,created_at:F,updated_at:F}).execute(),K=Array.isArray(G)?G[0]:G,O=Number(K?.insertId??K?.lastInsertRowid??K?.lastInsertId??0);return S.info(`Database notification sent to user ${z.userId}: ${z.type}`),{id:O,user_id:z.userId,type:z.type,data:JSON.stringify(z.data),read_at:null,created_at:F,updated_at:F}},async getUserNotifications(z){return await V.selectFrom("notifications").selectAll().where("user_id","=",z).orderBy("created_at","desc").execute()},async getUnreadNotifications(z){return await V.selectFrom("notifications").selectAll().where("user_id","=",z).where("read_at","is",null).orderBy("created_at","desc").execute()},async markAsRead(z){await V.updateTable("notifications").set({read_at:new Date().toISOString()}).where("id","=",z).execute()},async markAllAsRead(z){await V.updateTable("notifications").set({read_at:new Date().toISOString()}).where("user_id","=",z).where("read_at","is",null).execute()},async unreadCount(z){let F=await V.selectFrom("notifications").select(V.fn.countAll().as("count")).where("user_id","=",z).where("read_at","is",null).executeTakeFirst();return Number(F?.count??0)},async deleteNotification(z){await V.deleteFrom("notifications").where("id","=",z).execute()},async deleteAllNotifications(z){await V.deleteFrom("notifications").where("user_id","=",z).execute()}};import*as A from"@stacksjs/push";import*as L from"@stacksjs/sms";import{log as k}from"@stacksjs/cli";import{db as Y}from"@stacksjs/database";var Z="notification_preferences";function T(z,F){return F===null?z.whereNull("category"):z.where("category","=",F)}async function H(z,F){let J=new Map;try{let G=Y.selectFrom(Z).select(["channel","enabled","category"]).where("user_id","=",z);if(F!==void 0)G=G.where("category","=",F);let K=await G.execute();for(let O of K)J.set(O.channel,O.enabled===!0||O.enabled===1)}catch(G){k.debug?.(`[notifications] preferences lookup failed (table missing?): ${G.message}`)}return J}async function E(z,F,J,G){let K=new Date().toISOString(),O=G??null,R=Y.selectFrom(Z).select(["id"]).where("user_id","=",z).where("channel","=",F),U=await T(R,O).executeTakeFirst();if(U?.id){await Y.updateTable(Z).set({enabled:J,updated_at:K}).where("id","=",U.id).execute();return}await Y.insertInto(Z).values({user_id:z,channel:F,enabled:J,category:O,created_at:K,updated_at:K}).execute()}async function w(z,F){for(let J of F)await E(z,J.channel,J.enabled,J.category)}async function Q(z,F,J){let G=await H(z,J);if(J!==void 0){let K=await H(z,void 0);for(let[O,R]of K)if(!G.has(O))G.set(O,R)}return F.filter((K)=>{let O=G.get(K);return O===void 0?!0:O===!0})}var I=m;function B(z){return M[z||"slack"]}function j(z){if(z)try{return q.use(z)}catch(F){_.warn(`[notifications] email driver '${z}' not registered \u2014 falling back to default mail singleton (${F.message})`)}return q}function N(z){return L[z||"twilio"]}function b(){return A}function g(){return X}function Oz(){return W}function f(z,F){let J=z||I?.default||"email",G=F;switch(J){case"email":return j(G);case"chat":return B(G);case"sms":return N(G);case"database":return g();default:throw Error(`Notification type "${J}" is not supported`)}}function C(z){return!!z&&typeof z==="object"&&"send"in z&&typeof z.send==="function"}async function Rz(z,F,J=["email"],G={}){let K=J;if(z.userId&&!G.ignorePreferences)try{K=await Q(z.userId,J,G.category)}catch(R){_.warn(`[notify] preferences filter failed, sending all channels: ${R.message}`)}return(await Promise.allSettled(K.map(async(R)=>{switch(R){case"email":{if(!z.email)throw Error("[notify] email channel requires recipient.email");await j().send({to:z.email,subject:F.subject??"",text:F.body,html:`<p>${h(F.body)}</p>`});break}case"sms":{if(!z.phone)throw Error("[notify] sms channel requires recipient.phone");let U=N();if(!C(U))throw Error("[notify] sms channel is not configured: no usable SMS driver with a send() method");await U.send({to:z.phone,body:F.body});break}case"chat":{let U=B();if(!C(U))throw Error("[notify] chat channel is not configured: no usable chat driver with a send() method");await U.send({body:F.body});break}case"database":{if(!z.userId)throw Error("[notify] database channel requires recipient.userId");await X.send({userId:z.userId,type:F.subject||"notification",data:{body:F.body,...F.data}});break}case"push":{if(!z.pushTokens||Array.isArray(z.pushTokens)&&z.pushTokens.length===0)throw Error("[notify] push channel requires recipient.pushTokens");await b().send(z.pushTokens,{title:F.subject,body:F.body,data:F.data});break}case"broadcast":{await W.send({channel:z.broadcastChannel,userId:z.userId,event:F.subject??"notification",data:{body:F.body,...F.data}});break}default:throw Error(`Unsupported notification channel: ${R}`)}}))).map((R,U)=>{let $=K[U];if(!$)throw Error(`Missing notification channel for result ${U}`);if(R.status==="rejected"){let D=R.reason instanceof Error?R.reason.message:String(R.reason);_.warn(`[notify] ${$} channel failed: ${D}`)}return{channel:$,success:R.status==="fulfilled",error:R.status==="rejected"?R.reason:void 0}})}function Uz(){return f()}function h(z){return String(z??"").replace(/[&<>"']/g,(F)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[F])}export{T as wherePreferenceCategory,N as useSMS,b as usePush,f as useNotification,j as useEmail,g as useDatabase,B as useChat,Oz as useBroadcast,E as setNotificationPreference,Rz as notify,Uz as notification,H as getNotificationPreferences,Q as filterChannelsByPreferences,w as bulkSetPreferences,X as DatabaseNotificationDriver,W as BroadcastNotificationDriver};
@@ -1,4 +1,5 @@
1
1
  import type { NotificationChannel } from './index';
2
+ export declare function wherePreferenceCategory<T extends PreferenceCategoryQuery<T>>(query: T, category: string | null): T;
2
3
  /**
3
4
  * Load all notification preferences for a user as a Map keyed by channel.
4
5
  * Returns `Map<channel, enabled>` — channels with no row recorded are
@@ -102,5 +103,12 @@ export declare interface NotificationPreferenceRow {
102
103
  created_at: string
103
104
  updated_at: string
104
105
  }
106
+ /** Apply the nullable preference category without generating `IS $n` SQL.
107
+ * Postgres requires `IS NULL`; query builders correctly express that through
108
+ * `whereNull()`, while a bound value remains a normal equality predicate. */
109
+ declare interface PreferenceCategoryQuery<T> {
110
+ where: (column: string, operator: string, value: string) => T
111
+ whereNull: (column: string) => T
112
+ }
105
113
  /** All channels that can be filtered through preferences. */
106
114
  export type PreferenceChannel = NotificationChannel | 'push';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/notifications",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.100",
5
+ "version": "0.70.102",
6
6
  "description": "The Stacks notifications integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -59,14 +59,14 @@
59
59
  "prepublishOnly": "bun run build"
60
60
  },
61
61
  "devDependencies": {
62
- "@stacksjs/chat": "0.70.100",
63
- "@stacksjs/cli": "0.70.100",
64
- "@stacksjs/config": "0.70.100",
62
+ "@stacksjs/chat": "0.70.102",
63
+ "@stacksjs/cli": "0.70.102",
64
+ "@stacksjs/config": "0.70.102",
65
65
  "better-dx": "^0.2.16",
66
- "@stacksjs/email": "0.70.100",
67
- "@stacksjs/error-handling": "0.70.100",
68
- "@stacksjs/push": "0.70.100",
69
- "@stacksjs/sms": "0.70.100",
70
- "@stacksjs/types": "0.70.100"
66
+ "@stacksjs/email": "0.70.102",
67
+ "@stacksjs/error-handling": "0.70.102",
68
+ "@stacksjs/push": "0.70.102",
69
+ "@stacksjs/sms": "0.70.102",
70
+ "@stacksjs/types": "0.70.102"
71
71
  }
72
72
  }