@stacksjs/notifications 0.70.45 → 0.70.53

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2023 Open Web Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import{log as Y}from"@stacksjs/cli";import{notification as A}from"@stacksjs/config";import{mail as M}from"@stacksjs/email";import*as $ from"@stacksjs/chat";import{log as C}from"@stacksjs/cli";import{db as R}from"@stacksjs/database";var U={async send(x){let z=new Date().toISOString(),F=await R.insertInto("notifications").values({user_id:x.userId,type:x.type,data:JSON.stringify(x.data),read_at:null,created_at:z,updated_at:z}).execute(),K=Number(F?.[0]?.insertId??F?.insertId??F?.lastInsertRowid??F?.lastInsertId??0);return C.info(`Database notification sent to user ${x.userId}: ${x.type}`),{id:K,user_id:x.userId,type:x.type,data:JSON.stringify(x.data),read_at:null,created_at:z,updated_at:z}},async getUserNotifications(x){return await R.selectFrom("notifications").selectAll().where("user_id","=",x).orderBy("created_at","desc").execute()},async getUnreadNotifications(x){return await R.selectFrom("notifications").selectAll().where("user_id","=",x).where("read_at","is",null).orderBy("created_at","desc").execute()},async markAsRead(x){await R.updateTable("notifications").set({read_at:new Date().toISOString()}).where("id","=",x).execute()},async markAllAsRead(x){await R.updateTable("notifications").set({read_at:new Date().toISOString()}).where("user_id","=",x).where("read_at","is",null).execute()},async unreadCount(x){let z=await R.selectFrom("notifications").select(R.fn.countAll().as("count")).where("user_id","=",x).where("read_at","is",null).executeTakeFirst();return Number(z?.count??0)},async deleteNotification(x){await R.deleteFrom("notifications").where("id","=",x).execute()},async deleteAllNotifications(x){await R.deleteFrom("notifications").where("user_id","=",x).execute()}};import*as g from"@stacksjs/email";import*as v from"@stacksjs/push";import*as H from"@stacksjs/sms";import{log as E}from"@stacksjs/cli";import{db as _}from"@stacksjs/database";var V="notification_preferences";async function W(x,z){let G=new Map;try{let F=_.selectFrom(V).select(["channel","enabled","category"]).where("user_id","=",x);if(z!==void 0)F=F.where("category","=",z);let K=await F.execute();for(let Q of K)G.set(Q.channel,Q.enabled===!0||Q.enabled===1)}catch(F){E.debug?.(`[notifications] preferences lookup failed (table missing?): ${F.message}`)}return G}async function q(x,z,G,F){let K=new Date().toISOString(),Q=F??null,J=_,O=await J.selectFrom(V).select(["id"]).where("user_id","=",x).where("channel","=",z).where("category",Q===null?"is":"=",Q).executeTakeFirst();if(O?.id){await J.updateTable(V).set({enabled:G,updated_at:K}).where("id","=",O.id).execute();return}await J.insertInto(V).values({user_id:x,channel:z,enabled:G,category:Q,created_at:K,updated_at:K}).execute()}async function j(x,z){for(let G of z)await q(x,G.channel,G.enabled,G.category)}async function X(x,z,G){let F=await W(x,G);if(G!==void 0){let K=await W(x,void 0);for(let[Q,J]of K)if(!F.has(Q))F.set(Q,J)}return z.filter((K)=>{let Q=F.get(K);return Q===void 0?!0:Q===!0})}var D=A;function L(x){return $[x||"slack"]}function T(x){if(x)try{return M.use(x)}catch(z){Y.warn(`[notifications] email driver '${x}' not registered \u2014 falling back to default mail singleton (${z.message})`)}return M}function m(x){return H[x||"twilio"]}function N(){return U}function S(x,z){let G=x||D?.default||"email",F=z;switch(G){case"email":return T(F);case"chat":return L(F);case"sms":return m(F);case"database":return N();default:throw Error(`Notification type "${G}" is not supported`)}}async function Gx(x,z,G=["email"],F={}){let K=G;if(x.userId&&!F.ignorePreferences)try{K=await X(x.userId,G,F.category)}catch(J){Y.warn(`[notify] preferences filter failed, sending all channels: ${J.message}`)}return(await Promise.allSettled(K.map(async(J)=>{switch(J){case"email":{if(!x.email)throw Error("[notify] email channel requires recipient.email");await T().send({to:x.email,subject:z.subject,text:z.body,html:`<p>${P(z.body)}</p>`});break}case"sms":{let O=m();if(O&&typeof O==="object"&&"send"in O){if(!x.phone)throw Error("[notify] sms channel requires recipient.phone");await O.send({to:x.phone,body:z.body})}break}case"chat":{let O=L();if(O&&typeof O==="object"&&"send"in O)await O.send({body:z.body});break}case"database":{if(!x.userId)throw Error("[notify] database channel requires recipient.userId");await U.send({userId:x.userId,type:z.subject||"notification",data:{body:z.body,...z.data}});break}case"push":throw Error("[notify] push channel is filtered by preferences but not yet wired into a default driver");default:throw Error(`Unsupported notification channel: ${J}`)}}))).map((J,O)=>{let Z=K[O];if(J.status==="rejected"){let B=J.reason instanceof Error?J.reason.message:String(J.reason);Y.warn(`[notify] ${Z} channel failed: ${B}`)}return{channel:Z,success:J.status==="fulfilled",error:J.status==="rejected"?J.reason:void 0}})}function Jx(){return S()}function P(x){return String(x??"").replace(/[&<>"']/g,(z)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[z])}export{m as useSMS,S as useNotification,T as useEmail,N as useDatabase,L as useChat,q as setNotificationPreference,Gx as notify,Jx as notification,W as getNotificationPreferences,X as filterChannelsByPreferences,j as bulkSetPreferences,U as DatabaseNotificationDriver};
2
+ var N=import.meta.require;import{log as $}from"@stacksjs/cli";import{notification as w}from"@stacksjs/config";import{mail as T}from"@stacksjs/email";import{log as _}from"@stacksjs/cli";var U={async send(x){let z=x.event??"notification",G=x.channel;if(!G)G=x.userId!==void 0?`private-user-${x.userId}`:"notifications";try{let F=await import("@stacksjs/realtime").catch(()=>null),J=F?.emit,K=F?.getServer;if(!J||!K)return{delivered:!1,channel:G,event:z,reason:"@stacksjs/realtime is not available"};if(!K())return{delivered:!1,channel:G,event:z,reason:"realtime server not running"};let O=x.private??x.userId!==void 0;return J(G,z,x.data,{private:O}),_.info(`[notifications] broadcast to '${G}' (${z})`),{delivered:!0,channel:G,event:z}}catch(F){let J=F instanceof Error?F.message:String(F);return _.warn(`[notifications] broadcast failed: ${J}`),{delivered:!1,channel:G,event:z,reason:J}}}};import*as q from"@stacksjs/chat";import{log as P}from"@stacksjs/cli";import{db as R}from"@stacksjs/database";var V={async send(x){let z=new Date().toISOString(),F=await R.insertInto("notifications").values({user_id:x.userId,type:x.type,data:JSON.stringify(x.data),read_at:null,created_at:z,updated_at:z}).execute(),J=Array.isArray(F)?F[0]:F,K=Number(J?.insertId??J?.lastInsertRowid??J?.lastInsertId??0);return P.info(`Database notification sent to user ${x.userId}: ${x.type}`),{id:K,user_id:x.userId,type:x.type,data:JSON.stringify(x.data),read_at:null,created_at:z,updated_at:z}},async getUserNotifications(x){return await R.selectFrom("notifications").selectAll().where("user_id","=",x).orderBy("created_at","desc").execute()},async getUnreadNotifications(x){return await R.selectFrom("notifications").selectAll().where("user_id","=",x).where("read_at","is",null).orderBy("created_at","desc").execute()},async markAsRead(x){await R.updateTable("notifications").set({read_at:new Date().toISOString()}).where("id","=",x).execute()},async markAllAsRead(x){await R.updateTable("notifications").set({read_at:new Date().toISOString()}).where("user_id","=",x).where("read_at","is",null).execute()},async unreadCount(x){let z=await R.selectFrom("notifications").select(R.fn.countAll().as("count")).where("user_id","=",x).where("read_at","is",null).executeTakeFirst();return Number(z?.count??0)},async deleteNotification(x){await R.deleteFrom("notifications").where("id","=",x).execute()},async deleteAllNotifications(x){await R.deleteFrom("notifications").where("user_id","=",x).execute()}};import*as o from"@stacksjs/email";import*as M from"@stacksjs/push";import*as A from"@stacksjs/sms";import{log as D}from"@stacksjs/cli";import{db as W}from"@stacksjs/database";var X="notification_preferences";async function Y(x,z){let G=new Map;try{let F=W.selectFrom(X).select(["channel","enabled","category"]).where("user_id","=",x);if(z!==void 0)F=F.where("category","=",z);let J=await F.execute();for(let K of J)G.set(K.channel,K.enabled===!0||K.enabled===1)}catch(F){D.debug?.(`[notifications] preferences lookup failed (table missing?): ${F.message}`)}return G}async function L(x,z,G,F){let J=new Date().toISOString(),K=F??null,O=await W.selectFrom(X).select(["id"]).where("user_id","=",x).where("channel","=",z).where("category",K===null?"is":"=",K).executeTakeFirst();if(O?.id){await W.updateTable(X).set({enabled:G,updated_at:J}).where("id","=",O.id).execute();return}await W.insertInto(X).values({user_id:x,channel:z,enabled:G,category:K,created_at:J,updated_at:J}).execute()}async function S(x,z){for(let G of z)await L(x,G.channel,G.enabled,G.category)}async function Z(x,z,G){let F=await Y(x,G);if(G!==void 0){let J=await Y(x,void 0);for(let[K,O]of J)if(!F.has(K))F.set(K,O)}return z.filter((J)=>{let K=F.get(J);return K===void 0?!0:K===!0})}var k=w;function E(x){return q[x||"slack"]}function B(x){if(x)try{return T.use(x)}catch(z){$.warn(`[notifications] email driver '${x}' not registered \u2014 falling back to default mail singleton (${z.message})`)}return T}function j(x){return A[x||"twilio"]}function I(){return M}function f(){return V}function Yx(){return U}function g(x,z){let G=x||k?.default||"email",F=z;switch(G){case"email":return B(F);case"chat":return E(F);case"sms":return j(F);case"database":return f();default:throw Error(`Notification type "${G}" is not supported`)}}function C(x){return!!x&&typeof x==="object"&&"send"in x&&typeof x.send==="function"}async function Zx(x,z,G=["email"],F={}){let J=G;if(x.userId&&!F.ignorePreferences)try{J=await Z(x.userId,G,F.category)}catch(O){$.warn(`[notify] preferences filter failed, sending all channels: ${O.message}`)}return(await Promise.allSettled(J.map(async(O)=>{switch(O){case"email":{if(!x.email)throw Error("[notify] email channel requires recipient.email");await B().send({to:x.email,subject:z.subject,text:z.body,html:`<p>${b(z.body)}</p>`});break}case"sms":{if(!x.phone)throw Error("[notify] sms channel requires recipient.phone");let Q=j();if(!C(Q))throw Error("[notify] sms channel is not configured: no usable SMS driver with a send() method");await Q.send({to:x.phone,body:z.body});break}case"chat":{let Q=E();if(!C(Q))throw Error("[notify] chat channel is not configured: no usable chat driver with a send() method");await Q.send({body:z.body});break}case"database":{if(!x.userId)throw Error("[notify] database channel requires recipient.userId");await V.send({userId:x.userId,type:z.subject||"notification",data:{body:z.body,...z.data}});break}case"push":{if(!x.pushTokens||Array.isArray(x.pushTokens)&&x.pushTokens.length===0)throw Error("[notify] push channel requires recipient.pushTokens");await I().send(x.pushTokens,{title:z.subject,body:z.body,data:z.data});break}case"broadcast":{await U.send({channel:x.broadcastChannel,userId:x.userId,event:z.subject??"notification",data:{body:z.body,...z.data}});break}default:throw Error(`Unsupported notification channel: ${O}`)}}))).map((O,Q)=>{let H=J[Q];if(O.status==="rejected"){let m=O.reason instanceof Error?O.reason.message:String(O.reason);$.warn(`[notify] ${H} channel failed: ${m}`)}return{channel:H,success:O.status==="fulfilled",error:O.status==="rejected"?O.reason:void 0}})}function $x(){return g()}function b(x){return String(x??"").replace(/[&<>"']/g,(z)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[z])}export{j as useSMS,I as usePush,g as useNotification,B as useEmail,f as useDatabase,E as useChat,Yx as useBroadcast,L as setNotificationPreference,Zx as notify,$x as notification,Y as getNotificationPreferences,Z as filterChannelsByPreferences,S as bulkSetPreferences,V as DatabaseNotificationDriver,U as BroadcastNotificationDriver};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @defaultValue
3
+ * ```ts
4
+ * {
5
+ * send: (options?: BroadcastNotificationOptions): Promise<BroadcastNotificationResult> {
6
+ * const event) => Promise<(null)
7
+ * const emit = (mod as { emit?: (channel: string, event: string, data?: unknown, options?: { private?: boolean })) => any>
8
+ * }
9
+ * ```
10
+ */
11
+ export declare const BroadcastNotificationDriver: {
12
+ /**
13
+ * Emit the notification over the realtime channel. Resolves with
14
+ * `delivered: false` (and a reason) when no realtime server is
15
+ * available rather than throwing — fire-and-forget is the right shape
16
+ * for a notification channel that ships transports best-effort.
17
+ */
18
+ send: (options: BroadcastNotificationOptions) => Promise<BroadcastNotificationResult>
19
+ };
20
+ /** Options accepted by the broadcast driver. */
21
+ export declare interface BroadcastNotificationOptions {
22
+ channel?: string
23
+ userId?: number
24
+ event?: string
25
+ data?: Record<string, unknown>
26
+ private?: boolean
27
+ }
28
+ export declare interface BroadcastNotificationResult {
29
+ delivered: boolean
30
+ channel: string
31
+ event: string
32
+ reason?: string
33
+ }
@@ -24,6 +24,10 @@ export declare const DatabaseNotificationDriver: {
24
24
  deleteNotification: (id: number) => Promise<void>;
25
25
  deleteAllNotifications: (userId: number) => Promise<void>
26
26
  };
27
+ // `./database-schema.d.ts` augments `@stacksjs/database`'s
28
+ // `DatabaseSchema` with the `notifications` + `notification_preferences`
29
+ // tables so the chain calls below type-check without per-call `as any`
30
+ // (Notif-3 follow-up to #1923 / #1937).
27
31
  export declare interface DatabaseNotification {
28
32
  id: number
29
33
  user_id: number
@@ -36,5 +40,5 @@ export declare interface DatabaseNotification {
36
40
  export declare interface CreateNotificationOptions {
37
41
  userId: number
38
42
  type: string
39
- data: Record<string, any>
43
+ data: Record<string, unknown>
40
44
  }
@@ -1,3 +1,4 @@
1
+ export * from './broadcast';
1
2
  export * from './chat';
2
3
  export * from './database';
3
4
  export * from './email';
@@ -1,7 +1,9 @@
1
- import { chat, email, sms } from './drivers/index';
1
+ import { BroadcastNotificationDriver } from './drivers/broadcast';
2
+ import { chat, email, push, sms } from './drivers/index';
2
3
  import { DatabaseNotificationDriver } from './drivers/database';
3
4
  import { filterChannelsByPreferences } from './preferences';
4
5
  import type { EmailMessage, EmailResult } from '@stacksjs/types';
6
+ export type { BroadcastNotificationOptions, BroadcastNotificationResult } from './drivers/broadcast';
5
7
  export type { CreateNotificationOptions, DatabaseNotification } from './drivers/database';
6
8
  export type { NotificationPreferenceRow, PreferenceChannel } from './preferences';
7
9
  export declare function useChat(driver?: string): typeof chat[keyof typeof chat];
@@ -20,35 +22,27 @@ export declare function useChat(driver?: string): typeof chat[keyof typeof chat]
20
22
  */
21
23
  export declare function useEmail(driver?: string): EmailTransport;
22
24
  export declare function useSMS(driver?: string): typeof sms[keyof typeof sms];
23
- export declare function useDatabase(): typeof DatabaseNotificationDriver;
24
- export declare function useNotification(typeParam?: string, driverParam?: string): typeof chat[keyof typeof chat] | typeof email[keyof typeof email] | typeof sms[keyof typeof sms] | typeof DatabaseNotificationDriver;
25
25
  /**
26
- * Send a notification through multiple channels simultaneously.
27
- * Uses Promise.allSettled so one channel failure doesn't block others.
28
- *
29
- * When `recipient.userId` is set and `options.ignorePreferences` is not
30
- * `true`, the channel list is first filtered through the user's
31
- * `notification_preferences` rows — any channel they've explicitly
32
- * disabled is silently skipped. Channels with no preference recorded fall
33
- * through as enabled (default-allow), so introducing this feature doesn't
34
- * regress notifications for users who haven't visited the preferences UI.
35
- *
36
- * For transactional notifications that must always go out (password
37
- * resets, security alerts, billing failures), pass
38
- * `{ ignorePreferences: true }`.
39
- *
40
- * @example
41
- * ```ts
42
- * // respects user preferences
43
- * await notify({ userId: 7, email: 'a@x' }, { body: 'New message' }, ['email', 'sms'])
26
+ * Push transport returns the `@stacksjs/push` namespace whose
27
+ * `send(to, notification, options)` dispatches to Expo or FCM based on
28
+ * `options.driver`. `notify()` calls it with the driver omitted so the
29
+ * push package's own default kicks in (`expo`). Apps that need to
30
+ * force FCM can call `push.send(...)` directly with `{ driver: 'fcm' }`.
44
31
  *
45
- * // bypasses preferences (security alert)
46
- * await notify({ userId: 7, email: 'a@x' }, { body: 'New login' }, ['email'], { ignorePreferences: true })
47
- *
48
- * // category-specific opt-out
49
- * await notify({ userId: 7, email: 'a@x' }, { body: 'Sale!' }, ['email'], { category: 'marketing' })
50
- * ```
32
+ * Previously the `'push'` switch arm in `notify()` threw a hardcoded
33
+ * "not yet wired into a default driver" error even though both drivers
34
+ * exist (stacksjs/stacks#1874 F-1) — this helper is the missing link.
51
35
  */
36
+ export declare function usePush(): typeof push;
37
+ export declare function useDatabase(): typeof DatabaseNotificationDriver;
38
+ /**
39
+ * Broadcast transport — returns the `BroadcastNotificationDriver` which
40
+ * fans out a notification payload over the realtime WebSocket layer
41
+ * (`@stacksjs/realtime` `emit()`). See {@link BroadcastNotificationDriver}
42
+ * for channel-naming rules. stacksjs/stacks#669.
43
+ */
44
+ export declare function useBroadcast(): typeof BroadcastNotificationDriver;
45
+ export declare function useNotification(typeParam?: string, driverParam?: string): typeof chat[keyof typeof chat] | typeof email[keyof typeof email] | typeof sms[keyof typeof sms] | typeof DatabaseNotificationDriver;
52
46
  export declare function notify(recipient: NotificationRecipient, payload: NotificationPayload, channels?: NotificationChannel[], options?: NotifyOptions): Promise<NotifyResult[]>;
53
47
  export declare function notification(): ReturnType<typeof useNotification>;
54
48
  /** Optional flags accepted by {@link notify}. */
@@ -71,13 +65,16 @@ export declare interface NotificationRecipient {
71
65
  email?: string
72
66
  phone?: string
73
67
  userId?: number
68
+ pushTokens?: string | string[]
69
+ broadcastChannel?: string
74
70
  }
75
71
  export declare interface NotifyResult {
76
72
  channel: NotificationChannel
77
73
  success: boolean
78
74
  error?: Error
79
75
  }
80
- export type NotificationChannel = 'email' | 'sms' | 'chat' | 'database' | 'push';
76
+ export type NotificationChannel = 'email' | 'sms' | 'chat' | 'database' | 'push' | 'broadcast';
77
+ export { BroadcastNotificationDriver } from './drivers/broadcast';
81
78
  export { DatabaseNotificationDriver } from './drivers/database';
82
79
  export {
83
80
  bulkSetPreferences,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/notifications",
3
3
  "type": "module",
4
- "version": "0.70.45",
4
+ "version": "0.70.53",
5
5
  "description": "The Stacks notifications integration.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -54,14 +54,14 @@
54
54
  "prepublishOnly": "bun run build"
55
55
  },
56
56
  "devDependencies": {
57
- "@stacksjs/chat": "^0.70.45",
58
- "@stacksjs/cli": "^0.70.45",
59
- "@stacksjs/config": "^0.70.45",
57
+ "@stacksjs/chat": "0.70.53",
58
+ "@stacksjs/cli": "0.70.53",
59
+ "@stacksjs/config": "0.70.53",
60
60
  "better-dx": "^0.2.12",
61
- "@stacksjs/email": "^0.70.45",
62
- "@stacksjs/error-handling": "^0.70.45",
63
- "@stacksjs/push": "^0.70.45",
64
- "@stacksjs/sms": "^0.70.45",
65
- "@stacksjs/types": "^0.70.45"
61
+ "@stacksjs/email": "0.70.53",
62
+ "@stacksjs/error-handling": "0.70.53",
63
+ "@stacksjs/push": "0.70.53",
64
+ "@stacksjs/sms": "0.70.53",
65
+ "@stacksjs/types": "0.70.53"
66
66
  }
67
67
  }