av6-core 1.5.19 → 1.6.0

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.
Files changed (3) hide show
  1. package/dist/index.js +136 -43
  2. package/dist/index.mjs +136 -43
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2369,6 +2369,42 @@ var uinConfigService = (uinDeps) => {
2369
2369
  };
2370
2370
  };
2371
2371
 
2372
+ // src/providers/app.provider.ts
2373
+ var ExpoAppNotificationProvider = class {
2374
+ constructor(logger = console, url = "https://api.expo.dev/v2/push/send") {
2375
+ this.logger = logger;
2376
+ this.url = url;
2377
+ }
2378
+ /**
2379
+ * Bulk send: one HTTP request with many messages.
2380
+ */
2381
+ async sendBulk(messages) {
2382
+ const payload = (messages ?? []).filter((m) => !!m?.to);
2383
+ if (!payload.length) return { ok: true, tickets: [] };
2384
+ try {
2385
+ const res = await fetch(this.url, {
2386
+ method: "POST",
2387
+ headers: { "Content-Type": "application/json" },
2388
+ body: JSON.stringify(payload)
2389
+ });
2390
+ const data = await res.json().catch(() => ({}));
2391
+ if (!res.ok) {
2392
+ return { ok: false, error: `Expo HTTP ${res.status} ${res.statusText}`, meta: data };
2393
+ }
2394
+ const tickets = data?.data ?? [];
2395
+ const anyError = Array.isArray(tickets) && tickets.some((t) => t?.status === "error");
2396
+ return {
2397
+ ok: !anyError,
2398
+ tickets,
2399
+ meta: data
2400
+ };
2401
+ } catch (e) {
2402
+ this.logger.error("[ExpoAppNotificationProvider] sendBulk failed", e);
2403
+ return { ok: false, error: e?.message ?? String(e) };
2404
+ }
2405
+ }
2406
+ };
2407
+
2372
2408
  // src/types/notification.ts
2373
2409
  var TemplateType = /* @__PURE__ */ ((TemplateType2) => {
2374
2410
  TemplateType2["EMAIL"] = "EMAIL";
@@ -2379,33 +2415,6 @@ var TemplateType = /* @__PURE__ */ ((TemplateType2) => {
2379
2415
  return TemplateType2;
2380
2416
  })(TemplateType || {});
2381
2417
 
2382
- // src/providers/app.provider.ts
2383
- var AppNotificationProvider = class {
2384
- async send(args) {
2385
- if (!args.recipient.appUserId) {
2386
- return {
2387
- ok: false,
2388
- provider: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
2389
- error: "Missing recipient.appUserId"
2390
- };
2391
- }
2392
- try {
2393
- const fake = { id: "push-abc", status: "queued" };
2394
- return {
2395
- ok: true,
2396
- provider: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
2397
- externalId: fake.id
2398
- };
2399
- } catch (err) {
2400
- return {
2401
- ok: false,
2402
- provider: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
2403
- error: err?.message ?? String(err)
2404
- };
2405
- }
2406
- }
2407
- };
2408
-
2409
2418
  // src/providers/email.providers.ts
2410
2419
  var import_nodemailer = __toESM(require("nodemailer"));
2411
2420
  var EmailProvider = class {
@@ -3397,33 +3406,117 @@ var NotificationService = class {
3397
3406
  });
3398
3407
  }
3399
3408
  }
3409
+ async getExpoTokensByUserIds(userIds) {
3410
+ if (!userIds.length) return /* @__PURE__ */ new Map();
3411
+ const rows = await this.prisma.session.findMany({
3412
+ where: {
3413
+ userId: { in: userIds },
3414
+ deviceNotificationToken: { not: null },
3415
+ logoutAt: null
3416
+ },
3417
+ select: { userId: true, deviceNotificationToken: true }
3418
+ });
3419
+ const map = /* @__PURE__ */ new Map();
3420
+ for (const r of rows) {
3421
+ const token = r.deviceNotificationToken?.trim();
3422
+ if (!token) continue;
3423
+ if (!map.has(r.userId)) map.set(r.userId, []);
3424
+ map.get(r.userId).push(token);
3425
+ }
3426
+ for (const [k, v] of map.entries()) map.set(k, Array.from(new Set(v)));
3427
+ return map;
3428
+ }
3400
3429
  async processAppChannel(args) {
3401
3430
  const { cfg, evt, deliveryId } = args;
3402
3431
  const tpl = await this.prisma.template.findUnique({ where: { id: cfg.appNotificationTemplateId } });
3403
3432
  if (!tpl) return;
3404
- const recipients = args.recipients;
3405
- if (!recipients.length) {
3406
- this.logger.info("[NotificationService] APP: no recipients resolved");
3433
+ let appUrlTpl = null;
3434
+ if (cfg.allowAppNotification && cfg.serviceEvent.allowAppNotification && cfg.webNotificationUrlTemplateId) {
3435
+ appUrlTpl = await this.prisma.template.findUnique({ where: { id: cfg.appNotificationUrlTemplateId } });
3436
+ }
3437
+ const userIds = (args.recipients ?? []).map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0);
3438
+ if (!userIds.length) {
3439
+ this.logger.info("[NotificationService] APP: no userIds resolved");
3407
3440
  return;
3408
3441
  }
3409
- const subject = renderTemplate(tpl.subject ?? "", evt.data ?? {});
3442
+ const title = renderTemplate(tpl.subject ?? "", evt.data ?? {});
3410
3443
  const body = renderTemplate(tpl.bodyText ?? "", evt.data ?? {});
3411
- const provider = new AppNotificationProvider();
3412
- await this.sendPerRecipient({
3413
- deliveryId,
3414
- notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3415
- recipients,
3416
- messageContent: body,
3417
- sendOne: async (recipient) => {
3418
- const res = await provider.send({
3419
- recipient: { appUserId: recipient },
3420
- subject,
3444
+ const route = appUrlTpl ? renderTemplate(appUrlTpl.bodyText ?? "", evt.data ?? {}) : null;
3445
+ const tokensMap = await this.getExpoTokensByUserIds(userIds);
3446
+ const expoProvider = new ExpoAppNotificationProvider(
3447
+ this.logger,
3448
+ cfg.serviceEvent.appNotificationApiUrl ?? void 0
3449
+ );
3450
+ const priority = cfg.priority === "PRIMARY" ? "high" : cfg.priority === "SECONDARY" ? "normal" : "default";
3451
+ const channelId = cfg.channelId ?? "default";
3452
+ const messages = [];
3453
+ const tokenToUserId = /* @__PURE__ */ new Map();
3454
+ for (const userId of userIds) {
3455
+ const tokens = tokensMap.get(userId) ?? [];
3456
+ for (const to of tokens) {
3457
+ messages.push({
3458
+ to,
3459
+ title: title || void 0,
3421
3460
  body,
3422
- priority: cfg.priority === "PRIMARY" ? "high" : cfg.priority === "SECONDARY" ? "normal" : "low"
3461
+ channelId,
3462
+ priority,
3463
+ data: route ? { route } : void 0
3423
3464
  });
3424
- return this.mapProviderResult(res);
3465
+ tokenToUserId.set(to, userId);
3425
3466
  }
3467
+ }
3468
+ if (!messages.length) {
3469
+ await this.prisma.eventDeliveryItem.createMany({
3470
+ data: userIds.map((uid) => ({
3471
+ deliveryId,
3472
+ notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3473
+ recipient: String(uid),
3474
+ messageContent: body,
3475
+ isSent: false,
3476
+ sentAt: null,
3477
+ error: "No deviceNotificationToken found for user"
3478
+ }))
3479
+ });
3480
+ return;
3481
+ }
3482
+ const expoRes = await expoProvider.sendBulk(messages);
3483
+ const tickets = Array.isArray(expoRes.tickets) ? expoRes.tickets : [];
3484
+ const now = /* @__PURE__ */ new Date();
3485
+ const rows = messages.map((m, idx) => {
3486
+ const t = tickets[idx];
3487
+ const isOk = t?.status === "ok";
3488
+ const errMsg = isOk ? null : t?.message ?? t?.details?.error ?? expoRes.error ?? "Expo push failed";
3489
+ const externalId = isOk ? t?.id ?? null : null;
3490
+ return {
3491
+ deliveryId,
3492
+ notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3493
+ recipient: m.to,
3494
+ // store token or store userId; token is more debuggable
3495
+ messageContent: body,
3496
+ thirdPartyResponse: t ?? void 0,
3497
+ isSent: !!isOk,
3498
+ sentAt: isOk ? now : null,
3499
+ externalId,
3500
+ error: errMsg
3501
+ };
3426
3502
  });
3503
+ await this.prisma.eventDeliveryItem.createMany({ data: rows });
3504
+ const usersWithToken = /* @__PURE__ */ new Set();
3505
+ for (const to of tokenToUserId.keys()) usersWithToken.add(tokenToUserId.get(to));
3506
+ const missingUsers = userIds.filter((u) => !usersWithToken.has(u));
3507
+ if (missingUsers.length) {
3508
+ await this.prisma.eventDeliveryItem.createMany({
3509
+ data: missingUsers.map((uid) => ({
3510
+ deliveryId,
3511
+ notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3512
+ recipient: String(uid),
3513
+ messageContent: body,
3514
+ isSent: false,
3515
+ sentAt: null,
3516
+ error: "No deviceNotificationToken found for user"
3517
+ }))
3518
+ });
3519
+ }
3427
3520
  }
3428
3521
  async processWebChannel(args) {
3429
3522
  const { cfg, evt, deliveryId } = args;
package/dist/index.mjs CHANGED
@@ -2319,6 +2319,42 @@ var uinConfigService = (uinDeps) => {
2319
2319
  };
2320
2320
  };
2321
2321
 
2322
+ // src/providers/app.provider.ts
2323
+ var ExpoAppNotificationProvider = class {
2324
+ constructor(logger = console, url = "https://api.expo.dev/v2/push/send") {
2325
+ this.logger = logger;
2326
+ this.url = url;
2327
+ }
2328
+ /**
2329
+ * Bulk send: one HTTP request with many messages.
2330
+ */
2331
+ async sendBulk(messages) {
2332
+ const payload = (messages ?? []).filter((m) => !!m?.to);
2333
+ if (!payload.length) return { ok: true, tickets: [] };
2334
+ try {
2335
+ const res = await fetch(this.url, {
2336
+ method: "POST",
2337
+ headers: { "Content-Type": "application/json" },
2338
+ body: JSON.stringify(payload)
2339
+ });
2340
+ const data = await res.json().catch(() => ({}));
2341
+ if (!res.ok) {
2342
+ return { ok: false, error: `Expo HTTP ${res.status} ${res.statusText}`, meta: data };
2343
+ }
2344
+ const tickets = data?.data ?? [];
2345
+ const anyError = Array.isArray(tickets) && tickets.some((t) => t?.status === "error");
2346
+ return {
2347
+ ok: !anyError,
2348
+ tickets,
2349
+ meta: data
2350
+ };
2351
+ } catch (e) {
2352
+ this.logger.error("[ExpoAppNotificationProvider] sendBulk failed", e);
2353
+ return { ok: false, error: e?.message ?? String(e) };
2354
+ }
2355
+ }
2356
+ };
2357
+
2322
2358
  // src/types/notification.ts
2323
2359
  var TemplateType = /* @__PURE__ */ ((TemplateType2) => {
2324
2360
  TemplateType2["EMAIL"] = "EMAIL";
@@ -2329,33 +2365,6 @@ var TemplateType = /* @__PURE__ */ ((TemplateType2) => {
2329
2365
  return TemplateType2;
2330
2366
  })(TemplateType || {});
2331
2367
 
2332
- // src/providers/app.provider.ts
2333
- var AppNotificationProvider = class {
2334
- async send(args) {
2335
- if (!args.recipient.appUserId) {
2336
- return {
2337
- ok: false,
2338
- provider: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
2339
- error: "Missing recipient.appUserId"
2340
- };
2341
- }
2342
- try {
2343
- const fake = { id: "push-abc", status: "queued" };
2344
- return {
2345
- ok: true,
2346
- provider: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
2347
- externalId: fake.id
2348
- };
2349
- } catch (err) {
2350
- return {
2351
- ok: false,
2352
- provider: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
2353
- error: err?.message ?? String(err)
2354
- };
2355
- }
2356
- }
2357
- };
2358
-
2359
2368
  // src/providers/email.providers.ts
2360
2369
  import nodemailer from "nodemailer";
2361
2370
  var EmailProvider = class {
@@ -3347,33 +3356,117 @@ var NotificationService = class {
3347
3356
  });
3348
3357
  }
3349
3358
  }
3359
+ async getExpoTokensByUserIds(userIds) {
3360
+ if (!userIds.length) return /* @__PURE__ */ new Map();
3361
+ const rows = await this.prisma.session.findMany({
3362
+ where: {
3363
+ userId: { in: userIds },
3364
+ deviceNotificationToken: { not: null },
3365
+ logoutAt: null
3366
+ },
3367
+ select: { userId: true, deviceNotificationToken: true }
3368
+ });
3369
+ const map = /* @__PURE__ */ new Map();
3370
+ for (const r of rows) {
3371
+ const token = r.deviceNotificationToken?.trim();
3372
+ if (!token) continue;
3373
+ if (!map.has(r.userId)) map.set(r.userId, []);
3374
+ map.get(r.userId).push(token);
3375
+ }
3376
+ for (const [k, v] of map.entries()) map.set(k, Array.from(new Set(v)));
3377
+ return map;
3378
+ }
3350
3379
  async processAppChannel(args) {
3351
3380
  const { cfg, evt, deliveryId } = args;
3352
3381
  const tpl = await this.prisma.template.findUnique({ where: { id: cfg.appNotificationTemplateId } });
3353
3382
  if (!tpl) return;
3354
- const recipients = args.recipients;
3355
- if (!recipients.length) {
3356
- this.logger.info("[NotificationService] APP: no recipients resolved");
3383
+ let appUrlTpl = null;
3384
+ if (cfg.allowAppNotification && cfg.serviceEvent.allowAppNotification && cfg.webNotificationUrlTemplateId) {
3385
+ appUrlTpl = await this.prisma.template.findUnique({ where: { id: cfg.appNotificationUrlTemplateId } });
3386
+ }
3387
+ const userIds = (args.recipients ?? []).map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0);
3388
+ if (!userIds.length) {
3389
+ this.logger.info("[NotificationService] APP: no userIds resolved");
3357
3390
  return;
3358
3391
  }
3359
- const subject = renderTemplate(tpl.subject ?? "", evt.data ?? {});
3392
+ const title = renderTemplate(tpl.subject ?? "", evt.data ?? {});
3360
3393
  const body = renderTemplate(tpl.bodyText ?? "", evt.data ?? {});
3361
- const provider = new AppNotificationProvider();
3362
- await this.sendPerRecipient({
3363
- deliveryId,
3364
- notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3365
- recipients,
3366
- messageContent: body,
3367
- sendOne: async (recipient) => {
3368
- const res = await provider.send({
3369
- recipient: { appUserId: recipient },
3370
- subject,
3394
+ const route = appUrlTpl ? renderTemplate(appUrlTpl.bodyText ?? "", evt.data ?? {}) : null;
3395
+ const tokensMap = await this.getExpoTokensByUserIds(userIds);
3396
+ const expoProvider = new ExpoAppNotificationProvider(
3397
+ this.logger,
3398
+ cfg.serviceEvent.appNotificationApiUrl ?? void 0
3399
+ );
3400
+ const priority = cfg.priority === "PRIMARY" ? "high" : cfg.priority === "SECONDARY" ? "normal" : "default";
3401
+ const channelId = cfg.channelId ?? "default";
3402
+ const messages = [];
3403
+ const tokenToUserId = /* @__PURE__ */ new Map();
3404
+ for (const userId of userIds) {
3405
+ const tokens = tokensMap.get(userId) ?? [];
3406
+ for (const to of tokens) {
3407
+ messages.push({
3408
+ to,
3409
+ title: title || void 0,
3371
3410
  body,
3372
- priority: cfg.priority === "PRIMARY" ? "high" : cfg.priority === "SECONDARY" ? "normal" : "low"
3411
+ channelId,
3412
+ priority,
3413
+ data: route ? { route } : void 0
3373
3414
  });
3374
- return this.mapProviderResult(res);
3415
+ tokenToUserId.set(to, userId);
3375
3416
  }
3417
+ }
3418
+ if (!messages.length) {
3419
+ await this.prisma.eventDeliveryItem.createMany({
3420
+ data: userIds.map((uid) => ({
3421
+ deliveryId,
3422
+ notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3423
+ recipient: String(uid),
3424
+ messageContent: body,
3425
+ isSent: false,
3426
+ sentAt: null,
3427
+ error: "No deviceNotificationToken found for user"
3428
+ }))
3429
+ });
3430
+ return;
3431
+ }
3432
+ const expoRes = await expoProvider.sendBulk(messages);
3433
+ const tickets = Array.isArray(expoRes.tickets) ? expoRes.tickets : [];
3434
+ const now = /* @__PURE__ */ new Date();
3435
+ const rows = messages.map((m, idx) => {
3436
+ const t = tickets[idx];
3437
+ const isOk = t?.status === "ok";
3438
+ const errMsg = isOk ? null : t?.message ?? t?.details?.error ?? expoRes.error ?? "Expo push failed";
3439
+ const externalId = isOk ? t?.id ?? null : null;
3440
+ return {
3441
+ deliveryId,
3442
+ notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3443
+ recipient: m.to,
3444
+ // store token or store userId; token is more debuggable
3445
+ messageContent: body,
3446
+ thirdPartyResponse: t ?? void 0,
3447
+ isSent: !!isOk,
3448
+ sentAt: isOk ? now : null,
3449
+ externalId,
3450
+ error: errMsg
3451
+ };
3376
3452
  });
3453
+ await this.prisma.eventDeliveryItem.createMany({ data: rows });
3454
+ const usersWithToken = /* @__PURE__ */ new Set();
3455
+ for (const to of tokenToUserId.keys()) usersWithToken.add(tokenToUserId.get(to));
3456
+ const missingUsers = userIds.filter((u) => !usersWithToken.has(u));
3457
+ if (missingUsers.length) {
3458
+ await this.prisma.eventDeliveryItem.createMany({
3459
+ data: missingUsers.map((uid) => ({
3460
+ deliveryId,
3461
+ notificationType: "APP_NOTIFICATION" /* APP_NOTIFICATION */,
3462
+ recipient: String(uid),
3463
+ messageContent: body,
3464
+ isSent: false,
3465
+ sentAt: null,
3466
+ error: "No deviceNotificationToken found for user"
3467
+ }))
3468
+ });
3469
+ }
3377
3470
  }
3378
3471
  async processWebChannel(args) {
3379
3472
  const { cfg, evt, deliveryId } = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "av6-core",
3
- "version": "1.5.19",
3
+ "version": "1.6.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",