@stacksjs/notifications 0.70.44 → 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 +21 -0
- package/dist/index.js +1 -1
- package/dist/src/drivers/broadcast.d.ts +33 -0
- package/dist/src/drivers/database.d.ts +5 -1
- package/dist/src/drivers/index.d.ts +1 -0
- package/dist/src/index.d.ts +25 -28
- package/package.json +9 -9
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
|
|
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)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[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,
|
|
43
|
+
data: Record<string, unknown>
|
|
40
44
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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.
|
|
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": "
|
|
58
|
-
"@stacksjs/cli": "
|
|
59
|
-
"@stacksjs/config": "
|
|
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": "
|
|
62
|
-
"@stacksjs/error-handling": "
|
|
63
|
-
"@stacksjs/push": "
|
|
64
|
-
"@stacksjs/sms": "
|
|
65
|
-
"@stacksjs/types": "
|
|
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
|
}
|