@spfn/notification 0.1.0-beta.23 → 0.1.0-beta.24
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/server.js +89 -63
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -210,8 +210,8 @@ import { createHmac, createHash, timingSafeEqual } from "crypto";
|
|
|
210
210
|
function hashClickUrl(url) {
|
|
211
211
|
return createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
212
212
|
}
|
|
213
|
-
function toBase64Url(
|
|
214
|
-
return
|
|
213
|
+
function toBase64Url(buffer2) {
|
|
214
|
+
return buffer2.toString("base64url");
|
|
215
215
|
}
|
|
216
216
|
function fromBase64Url(str) {
|
|
217
217
|
return Buffer.from(str, "base64url").toString("utf8");
|
|
@@ -620,8 +620,8 @@ function registerBuiltinTemplates() {
|
|
|
620
620
|
}
|
|
621
621
|
|
|
622
622
|
// src/services/notification.service.ts
|
|
623
|
-
import { create, createMany, findOne, findMany, updateOne, count } from "@spfn/core/db";
|
|
624
|
-
import { desc, eq, and, gte, lte } from "drizzle-orm";
|
|
623
|
+
import { create, createMany, findOne, findMany, updateOne, count, getDatabase } from "@spfn/core/db";
|
|
624
|
+
import { desc, eq, and, gte, lte, count as drizzleCount } from "drizzle-orm";
|
|
625
625
|
|
|
626
626
|
// src/entities/schema.ts
|
|
627
627
|
import { createSchema } from "@spfn/core/db";
|
|
@@ -863,15 +863,33 @@ async function countNotifications(options = {}) {
|
|
|
863
863
|
return await count(notifications);
|
|
864
864
|
}
|
|
865
865
|
async function getNotificationStats(options = {}) {
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
866
|
+
const conditions = [];
|
|
867
|
+
if (options.channel) {
|
|
868
|
+
conditions.push(eq(notifications.channel, options.channel));
|
|
869
|
+
}
|
|
870
|
+
if (options.from) {
|
|
871
|
+
conditions.push(gte(notifications.createdAt, options.from));
|
|
872
|
+
}
|
|
873
|
+
if (options.to) {
|
|
874
|
+
conditions.push(lte(notifications.createdAt, options.to));
|
|
875
|
+
}
|
|
876
|
+
const rows = await getDatabase("read").select({ status: notifications.status, count: drizzleCount() }).from(notifications).where(conditions.length > 0 ? and(...conditions) : void 0).groupBy(notifications.status);
|
|
877
|
+
const stats = {
|
|
878
|
+
total: 0,
|
|
879
|
+
scheduled: 0,
|
|
880
|
+
pending: 0,
|
|
881
|
+
sent: 0,
|
|
882
|
+
failed: 0,
|
|
883
|
+
cancelled: 0
|
|
884
|
+
};
|
|
885
|
+
for (const row of rows) {
|
|
886
|
+
const n = Number(row.count);
|
|
887
|
+
stats.total += n;
|
|
888
|
+
if (row.status && row.status in stats) {
|
|
889
|
+
stats[row.status] = n;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return stats;
|
|
875
893
|
}
|
|
876
894
|
async function findScheduledNotifications(options = {}) {
|
|
877
895
|
const conditions = [eq(notifications.status, "scheduled")];
|
|
@@ -1809,11 +1827,11 @@ function IsTemplateLiteralFinite(schema) {
|
|
|
1809
1827
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/generate.mjs
|
|
1810
1828
|
var TemplateLiteralGenerateError = class extends TypeBoxError {
|
|
1811
1829
|
};
|
|
1812
|
-
function* GenerateReduce(
|
|
1813
|
-
if (
|
|
1814
|
-
return yield*
|
|
1815
|
-
for (const left of
|
|
1816
|
-
for (const right of GenerateReduce(
|
|
1830
|
+
function* GenerateReduce(buffer2) {
|
|
1831
|
+
if (buffer2.length === 1)
|
|
1832
|
+
return yield* buffer2[0];
|
|
1833
|
+
for (const left of buffer2[0]) {
|
|
1834
|
+
for (const right of GenerateReduce(buffer2.slice(1))) {
|
|
1817
1835
|
yield `${left}${right}`;
|
|
1818
1836
|
}
|
|
1819
1837
|
}
|
|
@@ -3646,15 +3664,8 @@ async function sendEmail(params) {
|
|
|
3646
3664
|
log2.error("Email send failed", { to: recipients, subject, error: result.error });
|
|
3647
3665
|
}
|
|
3648
3666
|
if (historyId && isHistoryEnabled()) {
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
await markNotificationSent(historyId, result.messageId);
|
|
3652
|
-
} else {
|
|
3653
|
-
await markNotificationFailed(historyId, result.error || "Unknown error");
|
|
3654
|
-
}
|
|
3655
|
-
} catch (error) {
|
|
3656
|
-
log2.warn("Failed to update notification history record", error);
|
|
3657
|
-
}
|
|
3667
|
+
const update = result.success ? markNotificationSent(historyId, result.messageId) : markNotificationFailed(historyId, result.error || "Unknown error");
|
|
3668
|
+
update.catch((error) => log2.warn("Failed to update notification history record", error));
|
|
3658
3669
|
}
|
|
3659
3670
|
return result;
|
|
3660
3671
|
}
|
|
@@ -3951,8 +3962,7 @@ async function sendSMS(params) {
|
|
|
3951
3962
|
};
|
|
3952
3963
|
}
|
|
3953
3964
|
const provider = getProvider2();
|
|
3954
|
-
const
|
|
3955
|
-
for (const recipient of recipients) {
|
|
3965
|
+
const sendOne = async (recipient) => {
|
|
3956
3966
|
const normalizedPhone = normalizePhoneNumber(recipient);
|
|
3957
3967
|
const internalParams = {
|
|
3958
3968
|
to: normalizedPhone,
|
|
@@ -3981,18 +3991,12 @@ async function sendSMS(params) {
|
|
|
3981
3991
|
log4.error("SMS send failed", { to: normalizedPhone, error: result.error });
|
|
3982
3992
|
}
|
|
3983
3993
|
if (historyId && isHistoryEnabled()) {
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
await markNotificationSent(historyId, result.messageId);
|
|
3987
|
-
} else {
|
|
3988
|
-
await markNotificationFailed(historyId, result.error || "Unknown error");
|
|
3989
|
-
}
|
|
3990
|
-
} catch (error) {
|
|
3991
|
-
log4.warn("Failed to update notification history record", error);
|
|
3992
|
-
}
|
|
3994
|
+
const update = result.success ? markNotificationSent(historyId, result.messageId) : markNotificationFailed(historyId, result.error || "Unknown error");
|
|
3995
|
+
update.catch((error) => log4.warn("Failed to update notification history record", error));
|
|
3993
3996
|
}
|
|
3994
|
-
|
|
3995
|
-
}
|
|
3997
|
+
return result;
|
|
3998
|
+
};
|
|
3999
|
+
const results = await runWithConcurrency(recipients, sendOne);
|
|
3996
4000
|
const allSuccess = results.every((r) => r.success);
|
|
3997
4001
|
const messageIds = results.filter((r) => r.messageId).map((r) => r.messageId).join(",");
|
|
3998
4002
|
const errors = results.filter((r) => r.error).map((r) => r.error).join("; ");
|
|
@@ -4271,15 +4275,8 @@ async function sendSlack(params) {
|
|
|
4271
4275
|
log6.error("Slack send failed", { error: result.error });
|
|
4272
4276
|
}
|
|
4273
4277
|
if (historyId && isHistoryEnabled()) {
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
await markNotificationSent(historyId, result.messageId);
|
|
4277
|
-
} else {
|
|
4278
|
-
await markNotificationFailed(historyId, result.error || "Unknown error");
|
|
4279
|
-
}
|
|
4280
|
-
} catch (error) {
|
|
4281
|
-
log6.warn("Failed to update notification history record", error);
|
|
4282
|
-
}
|
|
4278
|
+
const update = result.success ? markNotificationSent(historyId, result.messageId) : markNotificationFailed(historyId, result.error || "Unknown error");
|
|
4279
|
+
update.catch((error) => log6.warn("Failed to update notification history record", error));
|
|
4283
4280
|
}
|
|
4284
4281
|
return result;
|
|
4285
4282
|
}
|
|
@@ -4791,37 +4788,66 @@ import { route } from "@spfn/core/route";
|
|
|
4791
4788
|
import { defineRouter } from "@spfn/core/route";
|
|
4792
4789
|
|
|
4793
4790
|
// src/tracking/tracking.service.ts
|
|
4794
|
-
import {
|
|
4795
|
-
import { eq as eq2, and as and2, gte as gte2, lte as lte2, count as
|
|
4791
|
+
import { getDatabase as getDatabase2 } from "@spfn/core/db";
|
|
4792
|
+
import { eq as eq2, and as and2, gte as gte2, lte as lte2, count as drizzleCount2, countDistinct } from "drizzle-orm";
|
|
4796
4793
|
import { logger as logger7 } from "@spfn/core/logger";
|
|
4797
4794
|
var log7 = logger7.child("@spfn/notification:tracking");
|
|
4795
|
+
var FLUSH_SIZE = 200;
|
|
4796
|
+
var FLUSH_INTERVAL_MS = 2e3;
|
|
4797
|
+
var MAX_BUFFER = 1e4;
|
|
4798
|
+
var buffer = [];
|
|
4799
|
+
var flushTimer = null;
|
|
4800
|
+
async function flushTrackingEvents() {
|
|
4801
|
+
if (flushTimer) {
|
|
4802
|
+
clearTimeout(flushTimer);
|
|
4803
|
+
flushTimer = null;
|
|
4804
|
+
}
|
|
4805
|
+
if (buffer.length === 0) {
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
const batch = buffer.splice(0, buffer.length);
|
|
4809
|
+
try {
|
|
4810
|
+
await getDatabase2("write").insert(trackingEvents).values(batch);
|
|
4811
|
+
} catch (error) {
|
|
4812
|
+
log7.warn(`Failed to flush ${batch.length} tracking events`, error);
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
function enqueueTrackingEvent(event) {
|
|
4816
|
+
if (buffer.length >= MAX_BUFFER) {
|
|
4817
|
+
log7.warn("Tracking buffer full \u2014 dropping event");
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4820
|
+
buffer.push(event);
|
|
4821
|
+
if (buffer.length >= FLUSH_SIZE) {
|
|
4822
|
+
void flushTrackingEvents();
|
|
4823
|
+
} else if (!flushTimer) {
|
|
4824
|
+
flushTimer = setTimeout(() => void flushTrackingEvents(), FLUSH_INTERVAL_MS);
|
|
4825
|
+
flushTimer.unref?.();
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4798
4828
|
function recordOpenEvent(notificationId, meta) {
|
|
4799
|
-
|
|
4829
|
+
enqueueTrackingEvent({
|
|
4800
4830
|
notificationId,
|
|
4801
4831
|
type: "open",
|
|
4802
4832
|
ipAddress: meta?.ipAddress,
|
|
4803
4833
|
userAgent: meta?.userAgent
|
|
4804
|
-
}).catch((error) => {
|
|
4805
|
-
log7.warn("Failed to record open event", error);
|
|
4806
4834
|
});
|
|
4807
4835
|
}
|
|
4808
4836
|
function recordClickEvent(notificationId, linkIndex, linkUrl, meta) {
|
|
4809
|
-
|
|
4837
|
+
enqueueTrackingEvent({
|
|
4810
4838
|
notificationId,
|
|
4811
4839
|
type: "click",
|
|
4812
4840
|
linkUrl,
|
|
4813
4841
|
linkIndex,
|
|
4814
4842
|
ipAddress: meta?.ipAddress,
|
|
4815
4843
|
userAgent: meta?.userAgent
|
|
4816
|
-
}).catch((error) => {
|
|
4817
|
-
log7.warn("Failed to record click event", error);
|
|
4818
4844
|
});
|
|
4819
4845
|
}
|
|
4820
4846
|
async function getTrackingStats(notificationId) {
|
|
4821
|
-
const db =
|
|
4847
|
+
const db = getDatabase2("read");
|
|
4822
4848
|
const rows = await db.select({
|
|
4823
4849
|
type: trackingEvents.type,
|
|
4824
|
-
total:
|
|
4850
|
+
total: drizzleCount2(),
|
|
4825
4851
|
unique: countDistinct(trackingEvents.ipAddress)
|
|
4826
4852
|
}).from(trackingEvents).where(eq2(trackingEvents.notificationId, notificationId)).groupBy(trackingEvents.type);
|
|
4827
4853
|
const openRow = rows.find((r) => r.type === "open");
|
|
@@ -4834,7 +4860,7 @@ async function getTrackingStats(notificationId) {
|
|
|
4834
4860
|
};
|
|
4835
4861
|
}
|
|
4836
4862
|
async function getEngagementStats(options = {}) {
|
|
4837
|
-
const db =
|
|
4863
|
+
const db = getDatabase2("read");
|
|
4838
4864
|
const sentConditions = [eq2(notifications.status, "sent")];
|
|
4839
4865
|
if (options.channel) {
|
|
4840
4866
|
sentConditions.push(eq2(notifications.channel, options.channel));
|
|
@@ -4845,7 +4871,7 @@ async function getEngagementStats(options = {}) {
|
|
|
4845
4871
|
if (options.to) {
|
|
4846
4872
|
sentConditions.push(lte2(notifications.createdAt, options.to));
|
|
4847
4873
|
}
|
|
4848
|
-
const [sentResult] = await db.select({ count:
|
|
4874
|
+
const [sentResult] = await db.select({ count: drizzleCount2() }).from(notifications).where(and2(...sentConditions));
|
|
4849
4875
|
const sent = Number(sentResult?.count ?? 0);
|
|
4850
4876
|
if (sent === 0) {
|
|
4851
4877
|
return { sent: 0, opened: 0, clicked: 0, openRate: 0, clickRate: 0 };
|
|
@@ -4880,11 +4906,11 @@ async function getEngagementStats(options = {}) {
|
|
|
4880
4906
|
};
|
|
4881
4907
|
}
|
|
4882
4908
|
async function getClickDetails(notificationId) {
|
|
4883
|
-
const db =
|
|
4909
|
+
const db = getDatabase2("read");
|
|
4884
4910
|
const rows = await db.select({
|
|
4885
4911
|
linkUrl: trackingEvents.linkUrl,
|
|
4886
4912
|
linkIndex: trackingEvents.linkIndex,
|
|
4887
|
-
totalClicks:
|
|
4913
|
+
totalClicks: drizzleCount2(),
|
|
4888
4914
|
uniqueClicks: countDistinct(trackingEvents.ipAddress)
|
|
4889
4915
|
}).from(trackingEvents).where(
|
|
4890
4916
|
and2(
|