90dc-core 1.15.9 → 1.16.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clients/FirebasePushNotificationClient.d.ts +124 -0
- package/dist/lib/clients/FirebasePushNotificationClient.d.ts.map +1 -0
- package/dist/lib/clients/FirebasePushNotificationClient.js +603 -0
- package/dist/lib/clients/FirebasePushNotificationClient.js.map +1 -0
- package/dist/lib/dbmodels/program/CoachExerciseNote.d.ts +1 -1
- package/dist/lib/dbmodels/program/CoachExerciseNote.d.ts.map +1 -1
- package/dist/lib/dbmodels/program/CoachExerciseNote.js +1 -1
- package/dist/lib/dbmodels/program/CoachExerciseNote.js.map +1 -1
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./lib/models/NotificationInterfaces.js";
|
|
|
6
6
|
export * from "./lib/dbmodels/index.js";
|
|
7
7
|
export { EmailClient } from "./lib/clients/EmailClient.js";
|
|
8
8
|
export { PushNotificationClient } from "./lib/clients/PushNotificationClient.js";
|
|
9
|
+
export { FirebasePushNotificationClient } from "./lib/clients/FirebasePushNotificationClient.js";
|
|
9
10
|
export type { SendTemplateEmailRequest, BatchTemplateEmailRequest, EmailResponse, } from "./lib/clients/EmailClient.js";
|
|
10
11
|
export type { NotificationPayload } from "./lib/clients/PushNotificationClient.js";
|
|
11
12
|
export { AuthenticationUtil } from "./lib/utils/AuthenticationUtil.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wCAAwC,CAAC;AAGvD,cAAc,yBAAyB,CAAA;AAGvC,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAA;AAChF,YAAY,EACV,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,GACd,MAAM,8BAA8B,CAAA;AACrC,YAAY,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAA;AAGlF,OAAO,EAAC,kBAAkB,EAAC,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAC,iBAAiB,EAAC,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAC,kBAAkB,EAAC,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAC,GAAG,EAAC,MAAM,uBAAuB,CAAA;AACzC,OAAO,EAAC,aAAa,EAAC,MAAM,8BAA8B,CAAA;AAC1D,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,2BAA2B,CAAA;AAGlC,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,0BAA0B,EAC1B,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,GAC3B,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAA;AAGxC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,wBAAwB,EACxB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,UAAU,EACX,MAAM,0BAA0B,CAAA;AAGjC,OAAO,EACL,eAAe,GAChB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAGvF,OAAO,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAC,cAAc,EAAE,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wCAAwC,CAAC;AAGvD,cAAc,yBAAyB,CAAA;AAGvC,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAA;AAChF,OAAO,EAAE,8BAA8B,EAAE,MAAM,iDAAiD,CAAA;AAChG,YAAY,EACV,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,GACd,MAAM,8BAA8B,CAAA;AACrC,YAAY,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAA;AAGlF,OAAO,EAAC,kBAAkB,EAAC,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAC,iBAAiB,EAAC,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAC,kBAAkB,EAAC,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAC,GAAG,EAAC,MAAM,uBAAuB,CAAA;AACzC,OAAO,EAAC,aAAa,EAAC,MAAM,8BAA8B,CAAA;AAC1D,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,2BAA2B,CAAA;AAGlC,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,0BAA0B,EAC1B,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,GAC3B,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAA;AAGxC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,wBAAwB,EACxB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,UAAU,EACX,MAAM,0BAA0B,CAAA;AAGjC,OAAO,EACL,eAAe,GAChB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAGvF,OAAO,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAC,cAAc,EAAE,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./lib/dbmodels/index.js";
|
|
|
9
9
|
//Clients
|
|
10
10
|
export { EmailClient } from "./lib/clients/EmailClient.js";
|
|
11
11
|
export { PushNotificationClient } from "./lib/clients/PushNotificationClient.js";
|
|
12
|
+
export { FirebasePushNotificationClient } from "./lib/clients/FirebasePushNotificationClient.js";
|
|
12
13
|
//Utils
|
|
13
14
|
export { AuthenticationUtil } from "./lib/utils/AuthenticationUtil.js";
|
|
14
15
|
export { NotificationsUtil } from "./lib/utils/NotificationsUtil.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["//Interfaces\nexport * from \"./lib/models/ProgramInterfaces.js\";\nexport * from \"./lib/models/ExerciseInterfaces.js\";\nexport * from \"./lib/models/WorkoutInterfaces.js\";\nexport * from \"./lib/models/UserInterfaces.js\";\nexport * from \"./lib/models/NotificationInterfaces.js\";\n\n//DB Models - Export all models and model arrays\nexport * from \"./lib/dbmodels/index.js\"\n\n//Clients\nexport { EmailClient } from \"./lib/clients/EmailClient.js\"\nexport { PushNotificationClient } from \"./lib/clients/PushNotificationClient.js\"\nexport type {\n SendTemplateEmailRequest,\n BatchTemplateEmailRequest,\n EmailResponse,\n} from \"./lib/clients/EmailClient.js\"\nexport type { NotificationPayload } from \"./lib/clients/PushNotificationClient.js\"\n\n//Utils\nexport {AuthenticationUtil} from \"./lib/utils/AuthenticationUtil.js\"\nexport {NotificationsUtil} from \"./lib/utils/NotificationsUtil.js\"\nexport {NotificationClient} from \"./lib/utils/NotificationClient.js\"\nexport {Log} from \"./lib/utils/Logger.js\"\nexport {SecretManager} from \"./lib/utils/SecretManager.js\"\nexport {\n initializeSentry,\n isSentryEnabled,\n scrubObject,\n captureRequestBody,\n extractUserContext,\n buildRequestContext,\n buildResponseContext,\n reportErrorToSentry,\n reportMessageToSentry,\n type ErrorReportOptions,\n type SeverityLevel,\n} from \"./lib/utils/SentryUtil.js\"\n\n//Testing Utilities\nexport {\n createMockContext,\n createMockUser,\n createAuthenticatedContext,\n mockDatabase,\n flushPromises,\n assertions,\n executeRoute,\n type BaseContext,\n type AuthenticatedContext,\n type RouterContext,\n type RouterMiddleware,\n type RouterLayer,\n type RouterLike,\n type SequelizeModelMethods,\n} from \"./lib/testing/testHelpers.js\"\n\n//Config\nexport {\n ConfigValidator,\n BaseConfigSchema,\n CommonSchemas,\n createConfig,\n ConfigurationError,\n type BaseConfig\n} from \"./lib/config/ConfigValidator.js\"\n\n//Errors\nexport {\n AppError,\n ValidationError,\n AuthenticationError,\n ForbiddenError,\n NotFoundError,\n ConflictError,\n UnprocessableEntityError,\n RateLimitError,\n InternalServerError,\n ServiceUnavailableError,\n DatabaseError,\n ExternalAPIError,\n isAppError,\n isOperationalError,\n toAppError\n} from \"./lib/Errors/AppError.js\"\n\n//Middlewares\nexport {\n ErrorMiddleware,\n} from \"./lib/middlewares/ErrorMiddleware.js\"\nexport { validate, type ValidationConfig } from \"./lib/middlewares/ValidationMiddleware.js\";\n\n//Controllers\nexport { BaseController, type RouteConfig } from \"./lib/controllers/BaseController.js\";\n\n\nexport {RedisClient} from \"./lib/classes/Redis.js\"\nexport {DatabaseClient, type DatabaseConfig} from \"./lib/classes/Database.js\""],"names":["EmailClient","PushNotificationClient","AuthenticationUtil","NotificationsUtil","NotificationClient","Log","SecretManager","initializeSentry","isSentryEnabled","scrubObject","captureRequestBody","extractUserContext","buildRequestContext","buildResponseContext","reportErrorToSentry","reportMessageToSentry","createMockContext","createMockUser","createAuthenticatedContext","mockDatabase","flushPromises","assertions","executeRoute","ConfigValidator","BaseConfigSchema","CommonSchemas","createConfig","ConfigurationError","AppError","ValidationError","AuthenticationError","ForbiddenError","NotFoundError","ConflictError","UnprocessableEntityError","RateLimitError","InternalServerError","ServiceUnavailableError","DatabaseError","ExternalAPIError","isAppError","isOperationalError","toAppError","ErrorMiddleware","validate","BaseController","RedisClient","DatabaseClient"],"mappings":"AAAA,YAAY;AACZ,cAAc,oCAAoC;AAClD,cAAc,qCAAqC;AACnD,cAAc,oCAAoC;AAClD,cAAc,iCAAiC;AAC/C,cAAc,yCAAyC;AAEvD,gDAAgD;AAChD,cAAc,iBAAyB;AAEvC,SAAS;AACT,SAASA,WAAW,QAAQ,+BAA8B;AAC1D,SAASC,sBAAsB,QAAQ,0CAAyC;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["//Interfaces\nexport * from \"./lib/models/ProgramInterfaces.js\";\nexport * from \"./lib/models/ExerciseInterfaces.js\";\nexport * from \"./lib/models/WorkoutInterfaces.js\";\nexport * from \"./lib/models/UserInterfaces.js\";\nexport * from \"./lib/models/NotificationInterfaces.js\";\n\n//DB Models - Export all models and model arrays\nexport * from \"./lib/dbmodels/index.js\"\n\n//Clients\nexport { EmailClient } from \"./lib/clients/EmailClient.js\"\nexport { PushNotificationClient } from \"./lib/clients/PushNotificationClient.js\"\nexport { FirebasePushNotificationClient } from \"./lib/clients/FirebasePushNotificationClient.js\"\nexport type {\n SendTemplateEmailRequest,\n BatchTemplateEmailRequest,\n EmailResponse,\n} from \"./lib/clients/EmailClient.js\"\nexport type { NotificationPayload } from \"./lib/clients/PushNotificationClient.js\"\n\n//Utils\nexport {AuthenticationUtil} from \"./lib/utils/AuthenticationUtil.js\"\nexport {NotificationsUtil} from \"./lib/utils/NotificationsUtil.js\"\nexport {NotificationClient} from \"./lib/utils/NotificationClient.js\"\nexport {Log} from \"./lib/utils/Logger.js\"\nexport {SecretManager} from \"./lib/utils/SecretManager.js\"\nexport {\n initializeSentry,\n isSentryEnabled,\n scrubObject,\n captureRequestBody,\n extractUserContext,\n buildRequestContext,\n buildResponseContext,\n reportErrorToSentry,\n reportMessageToSentry,\n type ErrorReportOptions,\n type SeverityLevel,\n} from \"./lib/utils/SentryUtil.js\"\n\n//Testing Utilities\nexport {\n createMockContext,\n createMockUser,\n createAuthenticatedContext,\n mockDatabase,\n flushPromises,\n assertions,\n executeRoute,\n type BaseContext,\n type AuthenticatedContext,\n type RouterContext,\n type RouterMiddleware,\n type RouterLayer,\n type RouterLike,\n type SequelizeModelMethods,\n} from \"./lib/testing/testHelpers.js\"\n\n//Config\nexport {\n ConfigValidator,\n BaseConfigSchema,\n CommonSchemas,\n createConfig,\n ConfigurationError,\n type BaseConfig\n} from \"./lib/config/ConfigValidator.js\"\n\n//Errors\nexport {\n AppError,\n ValidationError,\n AuthenticationError,\n ForbiddenError,\n NotFoundError,\n ConflictError,\n UnprocessableEntityError,\n RateLimitError,\n InternalServerError,\n ServiceUnavailableError,\n DatabaseError,\n ExternalAPIError,\n isAppError,\n isOperationalError,\n toAppError\n} from \"./lib/Errors/AppError.js\"\n\n//Middlewares\nexport {\n ErrorMiddleware,\n} from \"./lib/middlewares/ErrorMiddleware.js\"\nexport { validate, type ValidationConfig } from \"./lib/middlewares/ValidationMiddleware.js\";\n\n//Controllers\nexport { BaseController, type RouteConfig } from \"./lib/controllers/BaseController.js\";\n\n\nexport {RedisClient} from \"./lib/classes/Redis.js\"\nexport {DatabaseClient, type DatabaseConfig} from \"./lib/classes/Database.js\""],"names":["EmailClient","PushNotificationClient","FirebasePushNotificationClient","AuthenticationUtil","NotificationsUtil","NotificationClient","Log","SecretManager","initializeSentry","isSentryEnabled","scrubObject","captureRequestBody","extractUserContext","buildRequestContext","buildResponseContext","reportErrorToSentry","reportMessageToSentry","createMockContext","createMockUser","createAuthenticatedContext","mockDatabase","flushPromises","assertions","executeRoute","ConfigValidator","BaseConfigSchema","CommonSchemas","createConfig","ConfigurationError","AppError","ValidationError","AuthenticationError","ForbiddenError","NotFoundError","ConflictError","UnprocessableEntityError","RateLimitError","InternalServerError","ServiceUnavailableError","DatabaseError","ExternalAPIError","isAppError","isOperationalError","toAppError","ErrorMiddleware","validate","BaseController","RedisClient","DatabaseClient"],"mappings":"AAAA,YAAY;AACZ,cAAc,oCAAoC;AAClD,cAAc,qCAAqC;AACnD,cAAc,oCAAoC;AAClD,cAAc,iCAAiC;AAC/C,cAAc,yCAAyC;AAEvD,gDAAgD;AAChD,cAAc,iBAAyB;AAEvC,SAAS;AACT,SAASA,WAAW,QAAQ,+BAA8B;AAC1D,SAASC,sBAAsB,QAAQ,0CAAyC;AAChF,SAASC,8BAA8B,QAAQ,kDAAiD;AAQhG,OAAO;AACP,SAAQC,kBAAkB,QAAO,oCAAmC;AACpE,SAAQC,iBAAiB,QAAO,mCAAkC;AAClE,SAAQC,kBAAkB,QAAO,oCAAmC;AACpE,SAAQC,GAAG,QAAO,wBAAuB;AACzC,SAAQC,aAAa,QAAO,+BAA8B;AAC1D,SACEC,gBAAgB,EAChBC,eAAe,EACfC,WAAW,EACXC,kBAAkB,EAClBC,kBAAkB,EAClBC,mBAAmB,EACnBC,oBAAoB,EACpBC,mBAAmB,EACnBC,qBAAqB,QAGhB,4BAA2B;AAElC,mBAAmB;AACnB,SACEC,iBAAiB,EACjBC,cAAc,EACdC,0BAA0B,EAC1BC,YAAY,EACZC,aAAa,EACbC,UAAU,EACVC,YAAY,QAQP,+BAA8B;AAErC,QAAQ;AACR,SACEC,eAAe,EACfC,gBAAgB,EAChBC,aAAa,EACbC,YAAY,EACZC,kBAAkB,QAEb,kCAAiC;AAExC,QAAQ;AACR,SACEC,QAAQ,EACRC,eAAe,EACfC,mBAAmB,EACnBC,cAAc,EACdC,aAAa,EACbC,aAAa,EACbC,wBAAwB,EACxBC,cAAc,EACdC,mBAAmB,EACnBC,uBAAuB,EACvBC,aAAa,EACbC,gBAAgB,EAChBC,UAAU,EACVC,kBAAkB,EAClBC,UAAU,QACL,2BAA0B;AAEjC,aAAa;AACb,SACEC,eAAe,QACV,uCAAsC;AAC7C,SAASC,QAAQ,QAA+B,4CAA4C;AAE5F,aAAa;AACb,SAASC,cAAc,QAA0B,sCAAsC;AAGvF,SAAQC,WAAW,QAAO,yBAAwB;AAClD,SAAQC,cAAc,QAA4B,4BAA2B"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Push Notification Client
|
|
3
|
+
*
|
|
4
|
+
* Industry-standard implementation using Firebase Admin SDK for:
|
|
5
|
+
* - iOS (APNs via FCM)
|
|
6
|
+
* - Android (FCM)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multicast messaging (up to 500 tokens per request)
|
|
10
|
+
* - Topic-based messaging for efficient broadcasting
|
|
11
|
+
* - Automatic token validation and cleanup
|
|
12
|
+
* - Built-in retry logic and error handling
|
|
13
|
+
* - Message delivery tracking
|
|
14
|
+
* - Type-safe API
|
|
15
|
+
*/
|
|
16
|
+
export interface NotificationPayload {
|
|
17
|
+
title: string;
|
|
18
|
+
body: string;
|
|
19
|
+
imageUrl?: string;
|
|
20
|
+
sound?: string;
|
|
21
|
+
badge?: number;
|
|
22
|
+
data?: Record<string, string>;
|
|
23
|
+
redirectPath?: string;
|
|
24
|
+
}
|
|
25
|
+
interface SendResult {
|
|
26
|
+
successCount: number;
|
|
27
|
+
failureCount: number;
|
|
28
|
+
invalidTokens: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare class FirebasePushNotificationClient {
|
|
31
|
+
private static instance;
|
|
32
|
+
private config;
|
|
33
|
+
private logger;
|
|
34
|
+
private app;
|
|
35
|
+
private constructor();
|
|
36
|
+
static getInstance(): FirebasePushNotificationClient;
|
|
37
|
+
static resetInstance(): void;
|
|
38
|
+
private initializeApp;
|
|
39
|
+
shutdown(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Send notifications using multicast (up to 500 tokens at once)
|
|
42
|
+
* Automatically handles token validation and cleanup
|
|
43
|
+
*/
|
|
44
|
+
private sendMulticast;
|
|
45
|
+
/**
|
|
46
|
+
* Remove invalid device tokens from database
|
|
47
|
+
*/
|
|
48
|
+
private removeInvalidTokens;
|
|
49
|
+
/**
|
|
50
|
+
* Send notification to specific device tokens
|
|
51
|
+
*/
|
|
52
|
+
sendToTokens(tokens: string[], platform: "ios" | "android", notification: NotificationPayload): Promise<SendResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Send notification to a single user (all their devices)
|
|
55
|
+
*/
|
|
56
|
+
sendToUser(params: {
|
|
57
|
+
userUuid: string;
|
|
58
|
+
notification: NotificationPayload;
|
|
59
|
+
}): Promise<SendResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Send notification to multiple users
|
|
62
|
+
*/
|
|
63
|
+
sendToUsers(params: {
|
|
64
|
+
userUuids: string[];
|
|
65
|
+
notification: NotificationPayload;
|
|
66
|
+
}): Promise<SendResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe users to a topic for efficient group messaging
|
|
69
|
+
*/
|
|
70
|
+
subscribeToTopic(tokens: string[], topic: string): Promise<{
|
|
71
|
+
successCount: number;
|
|
72
|
+
failureCount: number;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Send notification to a topic (efficient for large groups)
|
|
76
|
+
*/
|
|
77
|
+
sendToTopic(params: {
|
|
78
|
+
topic: string;
|
|
79
|
+
notification: NotificationPayload;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
messageId: string;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Send notification using a template
|
|
85
|
+
*/
|
|
86
|
+
sendTemplateToUser(params: {
|
|
87
|
+
userUuid: string;
|
|
88
|
+
notificationType: string;
|
|
89
|
+
language?: string;
|
|
90
|
+
data?: Record<string, string>;
|
|
91
|
+
}): Promise<SendResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Send templated notification to multiple users
|
|
94
|
+
*/
|
|
95
|
+
sendTemplateToUsers(params: {
|
|
96
|
+
userUuids: string[];
|
|
97
|
+
notificationType: string;
|
|
98
|
+
language?: string;
|
|
99
|
+
data?: Record<string, string>;
|
|
100
|
+
}): Promise<SendResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Send notification to all users (uses topic for efficiency)
|
|
103
|
+
* Consider subscribing users to an "all_users" topic for better performance
|
|
104
|
+
*/
|
|
105
|
+
sendToAllUsers(params: {
|
|
106
|
+
notification: NotificationPayload;
|
|
107
|
+
batchSize?: number;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
totalSent: number;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Send notification to a user group
|
|
113
|
+
* Recommended: Use topics instead for better performance
|
|
114
|
+
*/
|
|
115
|
+
sendToGroup(params: {
|
|
116
|
+
notification: NotificationPayload;
|
|
117
|
+
group: "premium" | "free" | "trial";
|
|
118
|
+
batchSize?: number;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
totalSent: number;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
123
|
+
export {};
|
|
124
|
+
//# sourceMappingURL=FirebasePushNotificationClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FirebasePushNotificationClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/FirebasePushNotificationClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Push Notification Client
|
|
3
|
+
*
|
|
4
|
+
* Industry-standard implementation using Firebase Admin SDK for:
|
|
5
|
+
* - iOS (APNs via FCM)
|
|
6
|
+
* - Android (FCM)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Multicast messaging (up to 500 tokens per request)
|
|
10
|
+
* - Topic-based messaging for efficient broadcasting
|
|
11
|
+
* - Automatic token validation and cleanup
|
|
12
|
+
* - Built-in retry logic and error handling
|
|
13
|
+
* - Message delivery tracking
|
|
14
|
+
* - Type-safe API
|
|
15
|
+
*/ import * as admin from "firebase-admin";
|
|
16
|
+
import { CommonSchemas } from "../config/ConfigValidator.js";
|
|
17
|
+
import { ExternalAPIError } from "../Errors/AppError.js";
|
|
18
|
+
import { Log } from "../utils/Logger.js";
|
|
19
|
+
import { DeviceTokens } from "../dbmodels/user/DeviceTokens.js";
|
|
20
|
+
import { PersistedUser } from "../dbmodels/user/PersistedUser.js";
|
|
21
|
+
import { NotificationModels } from "../dbmodels/notifications/NotificationModels.js";
|
|
22
|
+
import { TranslatedNotification } from "../dbmodels/notifications/TranslatedNotification.js";
|
|
23
|
+
export class FirebasePushNotificationClient {
|
|
24
|
+
static instance;
|
|
25
|
+
config;
|
|
26
|
+
logger = Log.getInstance().extend("firebase-push");
|
|
27
|
+
app = null;
|
|
28
|
+
constructor(config){
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.initializeApp();
|
|
31
|
+
}
|
|
32
|
+
static getInstance() {
|
|
33
|
+
if (!FirebasePushNotificationClient.instance) {
|
|
34
|
+
const config = CommonSchemas.pushNotification.parse(process.env);
|
|
35
|
+
FirebasePushNotificationClient.instance = new FirebasePushNotificationClient(config);
|
|
36
|
+
}
|
|
37
|
+
return FirebasePushNotificationClient.instance;
|
|
38
|
+
}
|
|
39
|
+
static resetInstance() {
|
|
40
|
+
FirebasePushNotificationClient.instance?.shutdown();
|
|
41
|
+
FirebasePushNotificationClient.instance = undefined;
|
|
42
|
+
}
|
|
43
|
+
initializeApp() {
|
|
44
|
+
try {
|
|
45
|
+
// Check if app already exists
|
|
46
|
+
if (admin.apps.length > 0) {
|
|
47
|
+
this.app = admin.app();
|
|
48
|
+
this.logger.info("Using existing Firebase app");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.logger.info("Initializing Firebase Admin SDK", {
|
|
52
|
+
projectId: this.config.FCM_PROJECT_ID
|
|
53
|
+
});
|
|
54
|
+
this.app = admin.initializeApp({
|
|
55
|
+
credential: admin.credential.cert({
|
|
56
|
+
projectId: this.config.FCM_PROJECT_ID,
|
|
57
|
+
clientEmail: this.config.FCM_CLIENT_EMAIL,
|
|
58
|
+
privateKey: this.config.FCM_PRIVATE_KEY
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
this.logger.info("Firebase Admin SDK initialized successfully");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.logger.error("Failed to initialize Firebase Admin SDK", {
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
throw new ExternalAPIError("Failed to initialize Firebase", `Error: ${String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
shutdown() {
|
|
70
|
+
if (this.app) {
|
|
71
|
+
this.app.delete().catch((error)=>{
|
|
72
|
+
this.logger.error("Error shutting down Firebase app", {
|
|
73
|
+
error
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
this.app = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Send notifications using multicast (up to 500 tokens at once)
|
|
81
|
+
* Automatically handles token validation and cleanup
|
|
82
|
+
*/ async sendMulticast(tokens, notification, platform) {
|
|
83
|
+
if (tokens.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
successCount: 0,
|
|
86
|
+
failureCount: 0,
|
|
87
|
+
invalidTokens: []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const messaging = admin.messaging();
|
|
91
|
+
const invalidTokens = [];
|
|
92
|
+
let successCount = 0;
|
|
93
|
+
let failureCount = 0;
|
|
94
|
+
// Firebase allows max 500 tokens per multicast
|
|
95
|
+
const BATCH_SIZE = 500;
|
|
96
|
+
for(let i = 0; i < tokens.length; i += BATCH_SIZE){
|
|
97
|
+
const batch = tokens.slice(i, i + BATCH_SIZE);
|
|
98
|
+
const message = {
|
|
99
|
+
tokens: batch,
|
|
100
|
+
notification: {
|
|
101
|
+
title: notification.title,
|
|
102
|
+
body: notification.body,
|
|
103
|
+
...notification.imageUrl && {
|
|
104
|
+
imageUrl: notification.imageUrl
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
data: notification.data || {},
|
|
108
|
+
...platform === "ios" && {
|
|
109
|
+
apns: {
|
|
110
|
+
payload: {
|
|
111
|
+
aps: {
|
|
112
|
+
sound: notification.sound || "default",
|
|
113
|
+
...notification.badge !== undefined && {
|
|
114
|
+
badge: notification.badge
|
|
115
|
+
},
|
|
116
|
+
contentAvailable: true
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
fcmOptions: {
|
|
120
|
+
...notification.imageUrl && {
|
|
121
|
+
imageUrl: notification.imageUrl
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
...platform === "android" && {
|
|
127
|
+
android: {
|
|
128
|
+
priority: "high",
|
|
129
|
+
notification: {
|
|
130
|
+
sound: notification.sound || "default",
|
|
131
|
+
channelId: "default",
|
|
132
|
+
...notification.imageUrl && {
|
|
133
|
+
imageUrl: notification.imageUrl
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
const response = await messaging.sendEachForMulticast(message);
|
|
141
|
+
successCount += response.successCount;
|
|
142
|
+
failureCount += response.failureCount;
|
|
143
|
+
// Identify and collect invalid tokens
|
|
144
|
+
response.responses.forEach((resp, idx)=>{
|
|
145
|
+
if (!resp.success && resp.error) {
|
|
146
|
+
const errorCode = resp.error.code;
|
|
147
|
+
// Token is invalid or unregistered
|
|
148
|
+
if (errorCode === "messaging/invalid-registration-token" || errorCode === "messaging/registration-token-not-registered") {
|
|
149
|
+
invalidTokens.push(batch[idx]);
|
|
150
|
+
}
|
|
151
|
+
this.logger.warn("Message send failed", {
|
|
152
|
+
token: batch[idx],
|
|
153
|
+
error: resp.error.message,
|
|
154
|
+
code: errorCode
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.logger.info("Multicast batch sent", {
|
|
159
|
+
batchSize: batch.length,
|
|
160
|
+
successCount: response.successCount,
|
|
161
|
+
failureCount: response.failureCount
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.logger.error("Multicast send error", {
|
|
165
|
+
error,
|
|
166
|
+
batchSize: batch.length
|
|
167
|
+
});
|
|
168
|
+
failureCount += batch.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Clean up invalid tokens from database
|
|
172
|
+
if (invalidTokens.length > 0) {
|
|
173
|
+
await this.removeInvalidTokens(invalidTokens);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
successCount,
|
|
177
|
+
failureCount,
|
|
178
|
+
invalidTokens
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Remove invalid device tokens from database
|
|
183
|
+
*/ async removeInvalidTokens(tokens) {
|
|
184
|
+
try {
|
|
185
|
+
const deleted = await DeviceTokens.destroy({
|
|
186
|
+
where: {
|
|
187
|
+
deviceToken: tokens
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
this.logger.info("Removed invalid tokens from database", {
|
|
191
|
+
count: deleted,
|
|
192
|
+
tokens: tokens.length
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.logger.error("Failed to remove invalid tokens", {
|
|
196
|
+
error
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Send notification to specific device tokens
|
|
202
|
+
*/ async sendToTokens(tokens, platform, notification) {
|
|
203
|
+
if (tokens.length === 0) {
|
|
204
|
+
this.logger.warn("No tokens provided, skipping notification");
|
|
205
|
+
return {
|
|
206
|
+
successCount: 0,
|
|
207
|
+
failureCount: 0,
|
|
208
|
+
invalidTokens: []
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
this.logger.info("Sending notification to tokens", {
|
|
212
|
+
tokenCount: tokens.length,
|
|
213
|
+
platform,
|
|
214
|
+
title: notification.title
|
|
215
|
+
});
|
|
216
|
+
return await this.sendMulticast(tokens, notification, platform);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Send notification to a single user (all their devices)
|
|
220
|
+
*/ async sendToUser(params) {
|
|
221
|
+
this.logger.info("Sending notification to user", {
|
|
222
|
+
userUuid: params.userUuid
|
|
223
|
+
});
|
|
224
|
+
const deviceTokens = await DeviceTokens.findAll({
|
|
225
|
+
where: {
|
|
226
|
+
userUuid: params.userUuid
|
|
227
|
+
},
|
|
228
|
+
attributes: [
|
|
229
|
+
"deviceToken",
|
|
230
|
+
"platform"
|
|
231
|
+
]
|
|
232
|
+
});
|
|
233
|
+
if (deviceTokens.length === 0) {
|
|
234
|
+
this.logger.warn("No device tokens found for user", {
|
|
235
|
+
userUuid: params.userUuid
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
successCount: 0,
|
|
239
|
+
failureCount: 0,
|
|
240
|
+
invalidTokens: []
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const iosTokens = deviceTokens.filter((dt)=>dt.platform === "ios").map((dt)=>dt.deviceToken);
|
|
244
|
+
const androidTokens = deviceTokens.filter((dt)=>dt.platform === "android").map((dt)=>dt.deviceToken);
|
|
245
|
+
const [iosResult, androidResult] = await Promise.all([
|
|
246
|
+
iosTokens.length > 0 ? this.sendToTokens(iosTokens, "ios", params.notification) : Promise.resolve({
|
|
247
|
+
successCount: 0,
|
|
248
|
+
failureCount: 0,
|
|
249
|
+
invalidTokens: []
|
|
250
|
+
}),
|
|
251
|
+
androidTokens.length > 0 ? this.sendToTokens(androidTokens, "android", params.notification) : Promise.resolve({
|
|
252
|
+
successCount: 0,
|
|
253
|
+
failureCount: 0,
|
|
254
|
+
invalidTokens: []
|
|
255
|
+
})
|
|
256
|
+
]);
|
|
257
|
+
return {
|
|
258
|
+
successCount: iosResult.successCount + androidResult.successCount,
|
|
259
|
+
failureCount: iosResult.failureCount + androidResult.failureCount,
|
|
260
|
+
invalidTokens: [
|
|
261
|
+
...iosResult.invalidTokens,
|
|
262
|
+
...androidResult.invalidTokens
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Send notification to multiple users
|
|
268
|
+
*/ async sendToUsers(params) {
|
|
269
|
+
this.logger.info("Sending notification to multiple users", {
|
|
270
|
+
count: params.userUuids.length
|
|
271
|
+
});
|
|
272
|
+
const deviceTokens = await DeviceTokens.findAll({
|
|
273
|
+
where: {
|
|
274
|
+
userUuid: params.userUuids
|
|
275
|
+
},
|
|
276
|
+
attributes: [
|
|
277
|
+
"deviceToken",
|
|
278
|
+
"platform"
|
|
279
|
+
]
|
|
280
|
+
});
|
|
281
|
+
if (deviceTokens.length === 0) {
|
|
282
|
+
this.logger.warn("No device tokens found for users");
|
|
283
|
+
return {
|
|
284
|
+
successCount: 0,
|
|
285
|
+
failureCount: 0,
|
|
286
|
+
invalidTokens: []
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const iosTokens = deviceTokens.filter((dt)=>dt.platform === "ios").map((dt)=>dt.deviceToken);
|
|
290
|
+
const androidTokens = deviceTokens.filter((dt)=>dt.platform === "android").map((dt)=>dt.deviceToken);
|
|
291
|
+
const [iosResult, androidResult] = await Promise.all([
|
|
292
|
+
iosTokens.length > 0 ? this.sendToTokens(iosTokens, "ios", params.notification) : Promise.resolve({
|
|
293
|
+
successCount: 0,
|
|
294
|
+
failureCount: 0,
|
|
295
|
+
invalidTokens: []
|
|
296
|
+
}),
|
|
297
|
+
androidTokens.length > 0 ? this.sendToTokens(androidTokens, "android", params.notification) : Promise.resolve({
|
|
298
|
+
successCount: 0,
|
|
299
|
+
failureCount: 0,
|
|
300
|
+
invalidTokens: []
|
|
301
|
+
})
|
|
302
|
+
]);
|
|
303
|
+
return {
|
|
304
|
+
successCount: iosResult.successCount + androidResult.successCount,
|
|
305
|
+
failureCount: iosResult.failureCount + androidResult.failureCount,
|
|
306
|
+
invalidTokens: [
|
|
307
|
+
...iosResult.invalidTokens,
|
|
308
|
+
...androidResult.invalidTokens
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Subscribe users to a topic for efficient group messaging
|
|
314
|
+
*/ async subscribeToTopic(tokens, topic) {
|
|
315
|
+
if (tokens.length === 0) {
|
|
316
|
+
return {
|
|
317
|
+
successCount: 0,
|
|
318
|
+
failureCount: 0
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
this.logger.info("Subscribing tokens to topic", {
|
|
322
|
+
tokenCount: tokens.length,
|
|
323
|
+
topic
|
|
324
|
+
});
|
|
325
|
+
try {
|
|
326
|
+
const response = await admin.messaging().subscribeToTopic(tokens, topic);
|
|
327
|
+
this.logger.info("Topic subscription complete", {
|
|
328
|
+
successCount: response.successCount,
|
|
329
|
+
failureCount: response.failureCount,
|
|
330
|
+
topic
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
successCount: response.successCount,
|
|
334
|
+
failureCount: response.failureCount
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
this.logger.error("Topic subscription error", {
|
|
338
|
+
error,
|
|
339
|
+
topic
|
|
340
|
+
});
|
|
341
|
+
throw new ExternalAPIError("Failed to subscribe to topic", `Topic: ${topic}, Error: ${String(error)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Send notification to a topic (efficient for large groups)
|
|
346
|
+
*/ async sendToTopic(params) {
|
|
347
|
+
this.logger.info("Sending notification to topic", {
|
|
348
|
+
topic: params.topic
|
|
349
|
+
});
|
|
350
|
+
const message = {
|
|
351
|
+
topic: params.topic,
|
|
352
|
+
notification: {
|
|
353
|
+
title: params.notification.title,
|
|
354
|
+
body: params.notification.body,
|
|
355
|
+
...params.notification.imageUrl && {
|
|
356
|
+
imageUrl: params.notification.imageUrl
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
data: params.notification.data || {},
|
|
360
|
+
apns: {
|
|
361
|
+
payload: {
|
|
362
|
+
aps: {
|
|
363
|
+
sound: params.notification.sound || "default",
|
|
364
|
+
...params.notification.badge !== undefined && {
|
|
365
|
+
badge: params.notification.badge
|
|
366
|
+
},
|
|
367
|
+
contentAvailable: true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
android: {
|
|
372
|
+
priority: "high",
|
|
373
|
+
notification: {
|
|
374
|
+
sound: params.notification.sound || "default",
|
|
375
|
+
channelId: "default"
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
const messageId = await admin.messaging().send(message);
|
|
381
|
+
this.logger.info("Topic notification sent", {
|
|
382
|
+
topic: params.topic,
|
|
383
|
+
messageId
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
messageId
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {
|
|
389
|
+
this.logger.error("Topic send error", {
|
|
390
|
+
error,
|
|
391
|
+
topic: params.topic
|
|
392
|
+
});
|
|
393
|
+
throw new ExternalAPIError("Failed to send topic notification", `Topic: ${params.topic}, Error: ${String(error)}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Send notification using a template
|
|
398
|
+
*/ async sendTemplateToUser(params) {
|
|
399
|
+
this.logger.info("Sending template notification to user", {
|
|
400
|
+
userUuid: params.userUuid,
|
|
401
|
+
notificationType: params.notificationType,
|
|
402
|
+
language: params.language
|
|
403
|
+
});
|
|
404
|
+
const includeOptions = {
|
|
405
|
+
model: TranslatedNotification,
|
|
406
|
+
as: "translations",
|
|
407
|
+
required: false
|
|
408
|
+
};
|
|
409
|
+
if (params.language) {
|
|
410
|
+
includeOptions.where = {
|
|
411
|
+
language: params.language
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const template = await NotificationModels.findOne({
|
|
415
|
+
where: {
|
|
416
|
+
type: params.notificationType
|
|
417
|
+
},
|
|
418
|
+
include: [
|
|
419
|
+
includeOptions
|
|
420
|
+
]
|
|
421
|
+
});
|
|
422
|
+
if (!template) {
|
|
423
|
+
throw new ExternalAPIError("Notification template not found", `Type: ${params.notificationType}`);
|
|
424
|
+
}
|
|
425
|
+
const translation = template.translations?.find((t)=>t.language === params.language);
|
|
426
|
+
let text = translation?.text || template.text;
|
|
427
|
+
if (params.data) {
|
|
428
|
+
Object.entries(params.data).forEach(([key, value])=>{
|
|
429
|
+
text = text.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return await this.sendToUser({
|
|
433
|
+
userUuid: params.userUuid,
|
|
434
|
+
notification: {
|
|
435
|
+
title: params.notificationType,
|
|
436
|
+
body: text,
|
|
437
|
+
...params.data && {
|
|
438
|
+
data: params.data
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Send templated notification to multiple users
|
|
445
|
+
*/ async sendTemplateToUsers(params) {
|
|
446
|
+
this.logger.info("Sending template notification to multiple users", {
|
|
447
|
+
count: params.userUuids.length,
|
|
448
|
+
notificationType: params.notificationType
|
|
449
|
+
});
|
|
450
|
+
const includeOptions = {
|
|
451
|
+
model: TranslatedNotification,
|
|
452
|
+
as: "translations",
|
|
453
|
+
required: false
|
|
454
|
+
};
|
|
455
|
+
if (params.language) {
|
|
456
|
+
includeOptions.where = {
|
|
457
|
+
language: params.language
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const template = await NotificationModels.findOne({
|
|
461
|
+
where: {
|
|
462
|
+
type: params.notificationType
|
|
463
|
+
},
|
|
464
|
+
include: [
|
|
465
|
+
includeOptions
|
|
466
|
+
]
|
|
467
|
+
});
|
|
468
|
+
if (!template) {
|
|
469
|
+
throw new ExternalAPIError("Notification template not found", `Type: ${params.notificationType}`);
|
|
470
|
+
}
|
|
471
|
+
const translation = template.translations?.find((t)=>t.language === params.language);
|
|
472
|
+
let text = translation?.text || template.text;
|
|
473
|
+
if (params.data) {
|
|
474
|
+
Object.entries(params.data).forEach(([key, value])=>{
|
|
475
|
+
text = text.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return await this.sendToUsers({
|
|
479
|
+
userUuids: params.userUuids,
|
|
480
|
+
notification: {
|
|
481
|
+
title: params.notificationType,
|
|
482
|
+
body: text,
|
|
483
|
+
...params.data && {
|
|
484
|
+
data: params.data
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Send notification to all users (uses topic for efficiency)
|
|
491
|
+
* Consider subscribing users to an "all_users" topic for better performance
|
|
492
|
+
*/ async sendToAllUsers(params) {
|
|
493
|
+
const batchSize = params.batchSize || 1000;
|
|
494
|
+
let offset = 0;
|
|
495
|
+
let totalSent = 0;
|
|
496
|
+
this.logger.info("Starting broadcast notification", {
|
|
497
|
+
batchSize
|
|
498
|
+
});
|
|
499
|
+
while(true){
|
|
500
|
+
const deviceTokens = await DeviceTokens.findAll({
|
|
501
|
+
limit: batchSize,
|
|
502
|
+
offset,
|
|
503
|
+
attributes: [
|
|
504
|
+
"deviceToken",
|
|
505
|
+
"platform"
|
|
506
|
+
]
|
|
507
|
+
});
|
|
508
|
+
if (deviceTokens.length === 0) {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
const iosTokens = deviceTokens.filter((dt)=>dt.platform === "ios").map((dt)=>dt.deviceToken);
|
|
512
|
+
const androidTokens = deviceTokens.filter((dt)=>dt.platform === "android").map((dt)=>dt.deviceToken);
|
|
513
|
+
const [iosResult, androidResult] = await Promise.all([
|
|
514
|
+
iosTokens.length > 0 ? this.sendToTokens(iosTokens, "ios", params.notification) : Promise.resolve({
|
|
515
|
+
successCount: 0,
|
|
516
|
+
failureCount: 0,
|
|
517
|
+
invalidTokens: []
|
|
518
|
+
}),
|
|
519
|
+
androidTokens.length > 0 ? this.sendToTokens(androidTokens, "android", params.notification) : Promise.resolve({
|
|
520
|
+
successCount: 0,
|
|
521
|
+
failureCount: 0,
|
|
522
|
+
invalidTokens: []
|
|
523
|
+
})
|
|
524
|
+
]);
|
|
525
|
+
totalSent += iosResult.successCount + androidResult.successCount;
|
|
526
|
+
offset += batchSize;
|
|
527
|
+
this.logger.info("Broadcast batch sent", {
|
|
528
|
+
batchSent: iosResult.successCount + androidResult.successCount,
|
|
529
|
+
totalSent
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
this.logger.info("Broadcast notification complete", {
|
|
533
|
+
totalSent
|
|
534
|
+
});
|
|
535
|
+
return {
|
|
536
|
+
totalSent
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Send notification to a user group
|
|
541
|
+
* Recommended: Use topics instead for better performance
|
|
542
|
+
*/ async sendToGroup(params) {
|
|
543
|
+
const batchSize = params.batchSize || 1000;
|
|
544
|
+
let offset = 0;
|
|
545
|
+
let totalSent = 0;
|
|
546
|
+
this.logger.info("Starting group notification", {
|
|
547
|
+
group: params.group,
|
|
548
|
+
batchSize
|
|
549
|
+
});
|
|
550
|
+
while(true){
|
|
551
|
+
const users = await PersistedUser.findAll({
|
|
552
|
+
where: {
|
|
553
|
+
subscriptionType: params.group
|
|
554
|
+
},
|
|
555
|
+
include: [
|
|
556
|
+
{
|
|
557
|
+
model: DeviceTokens,
|
|
558
|
+
as: "deviceTokens",
|
|
559
|
+
attributes: [
|
|
560
|
+
"deviceToken",
|
|
561
|
+
"platform"
|
|
562
|
+
]
|
|
563
|
+
}
|
|
564
|
+
],
|
|
565
|
+
limit: batchSize,
|
|
566
|
+
offset
|
|
567
|
+
});
|
|
568
|
+
if (users.length === 0) {
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
const allTokens = users.flatMap((user)=>user.deviceTokens || []);
|
|
572
|
+
const iosTokens = allTokens.filter((dt)=>dt.platform === "ios").map((dt)=>dt.deviceToken);
|
|
573
|
+
const androidTokens = allTokens.filter((dt)=>dt.platform === "android").map((dt)=>dt.deviceToken);
|
|
574
|
+
const [iosResult, androidResult] = await Promise.all([
|
|
575
|
+
iosTokens.length > 0 ? this.sendToTokens(iosTokens, "ios", params.notification) : Promise.resolve({
|
|
576
|
+
successCount: 0,
|
|
577
|
+
failureCount: 0,
|
|
578
|
+
invalidTokens: []
|
|
579
|
+
}),
|
|
580
|
+
androidTokens.length > 0 ? this.sendToTokens(androidTokens, "android", params.notification) : Promise.resolve({
|
|
581
|
+
successCount: 0,
|
|
582
|
+
failureCount: 0,
|
|
583
|
+
invalidTokens: []
|
|
584
|
+
})
|
|
585
|
+
]);
|
|
586
|
+
totalSent += iosResult.successCount + androidResult.successCount;
|
|
587
|
+
offset += batchSize;
|
|
588
|
+
this.logger.info("Group batch sent", {
|
|
589
|
+
batchSent: iosResult.successCount + androidResult.successCount,
|
|
590
|
+
totalSent
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
this.logger.info("Group notification complete", {
|
|
594
|
+
totalSent,
|
|
595
|
+
group: params.group
|
|
596
|
+
});
|
|
597
|
+
return {
|
|
598
|
+
totalSent
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
//# sourceMappingURL=FirebasePushNotificationClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/clients/FirebasePushNotificationClient.ts"],"sourcesContent":["/**\n * Firebase Push Notification Client\n *\n * Industry-standard implementation using Firebase Admin SDK for:\n * - iOS (APNs via FCM)\n * - Android (FCM)\n *\n * Features:\n * - Multicast messaging (up to 500 tokens per request)\n * - Topic-based messaging for efficient broadcasting\n * - Automatic token validation and cleanup\n * - Built-in retry logic and error handling\n * - Message delivery tracking\n * - Type-safe API\n */\n\nimport * as admin from \"firebase-admin\";\nimport { z } from \"zod\";\nimport { CommonSchemas } from \"../config/ConfigValidator.js\";\nimport { ExternalAPIError } from \"../Errors/AppError.js\";\nimport { Log } from \"../utils/Logger.js\";\nimport { DeviceTokens } from \"../dbmodels/user/DeviceTokens.js\";\nimport { PersistedUser } from \"../dbmodels/user/PersistedUser.js\";\nimport { NotificationModels } from \"../dbmodels/notifications/NotificationModels.js\";\nimport { TranslatedNotification } from \"../dbmodels/notifications/TranslatedNotification.js\";\n\ntype FirebaseConfig = z.infer<typeof CommonSchemas.pushNotification>;\n\nexport interface NotificationPayload {\n title: string;\n body: string;\n imageUrl?: string;\n sound?: string;\n badge?: number;\n data?: Record<string, string>;\n redirectPath?: string;\n}\n\ninterface SendResult {\n successCount: number;\n failureCount: number;\n invalidTokens: string[];\n}\n\nexport class FirebasePushNotificationClient {\n private static instance: FirebasePushNotificationClient;\n private config: FirebaseConfig;\n private logger = Log.getInstance().extend(\"firebase-push\");\n private app: admin.app.App | null = null;\n\n private constructor(config: FirebaseConfig) {\n this.config = config;\n this.initializeApp();\n }\n\n public static getInstance(): FirebasePushNotificationClient {\n if (!FirebasePushNotificationClient.instance) {\n const config = CommonSchemas.pushNotification.parse(process.env);\n FirebasePushNotificationClient.instance = new FirebasePushNotificationClient(config);\n }\n return FirebasePushNotificationClient.instance;\n }\n\n public static resetInstance(): void {\n FirebasePushNotificationClient.instance?.shutdown();\n FirebasePushNotificationClient.instance = undefined as any;\n }\n\n private initializeApp(): void {\n try {\n // Check if app already exists\n if (admin.apps.length > 0) {\n this.app = admin.app();\n this.logger.info(\"Using existing Firebase app\");\n return;\n }\n\n this.logger.info(\"Initializing Firebase Admin SDK\", {\n projectId: this.config.FCM_PROJECT_ID,\n });\n\n this.app = admin.initializeApp({\n credential: admin.credential.cert({\n projectId: this.config.FCM_PROJECT_ID,\n clientEmail: this.config.FCM_CLIENT_EMAIL,\n privateKey: this.config.FCM_PRIVATE_KEY,\n }),\n });\n\n this.logger.info(\"Firebase Admin SDK initialized successfully\");\n } catch (error) {\n this.logger.error(\"Failed to initialize Firebase Admin SDK\", { error });\n throw new ExternalAPIError(\n \"Failed to initialize Firebase\",\n `Error: ${String(error)}`\n );\n }\n }\n\n public shutdown(): void {\n if (this.app) {\n this.app.delete().catch((error) => {\n this.logger.error(\"Error shutting down Firebase app\", { error });\n });\n this.app = null;\n }\n }\n\n /**\n * Send notifications using multicast (up to 500 tokens at once)\n * Automatically handles token validation and cleanup\n */\n private async sendMulticast(\n tokens: string[],\n notification: NotificationPayload,\n platform?: \"ios\" | \"android\"\n ): Promise<SendResult> {\n if (tokens.length === 0) {\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n const messaging = admin.messaging();\n const invalidTokens: string[] = [];\n let successCount = 0;\n let failureCount = 0;\n\n // Firebase allows max 500 tokens per multicast\n const BATCH_SIZE = 500;\n\n for (let i = 0; i < tokens.length; i += BATCH_SIZE) {\n const batch = tokens.slice(i, i + BATCH_SIZE);\n\n const message: admin.messaging.MulticastMessage = {\n tokens: batch,\n notification: {\n title: notification.title,\n body: notification.body,\n ...(notification.imageUrl && { imageUrl: notification.imageUrl }),\n },\n data: notification.data || {},\n ...(platform === \"ios\" && {\n apns: {\n payload: {\n aps: {\n sound: notification.sound || \"default\",\n ...(notification.badge !== undefined && { badge: notification.badge }),\n contentAvailable: true,\n },\n },\n fcmOptions: {\n ...(notification.imageUrl && { imageUrl: notification.imageUrl }),\n },\n },\n }),\n ...(platform === \"android\" && {\n android: {\n priority: \"high\",\n notification: {\n sound: notification.sound || \"default\",\n channelId: \"default\",\n ...(notification.imageUrl && { imageUrl: notification.imageUrl }),\n },\n },\n }),\n };\n\n try {\n const response = await messaging.sendEachForMulticast(message);\n\n successCount += response.successCount;\n failureCount += response.failureCount;\n\n // Identify and collect invalid tokens\n response.responses.forEach((resp, idx) => {\n if (!resp.success && resp.error) {\n const errorCode = resp.error.code;\n\n // Token is invalid or unregistered\n if (\n errorCode === \"messaging/invalid-registration-token\" ||\n errorCode === \"messaging/registration-token-not-registered\"\n ) {\n invalidTokens.push(batch[idx]);\n }\n\n this.logger.warn(\"Message send failed\", {\n token: batch[idx],\n error: resp.error.message,\n code: errorCode,\n });\n }\n });\n\n this.logger.info(\"Multicast batch sent\", {\n batchSize: batch.length,\n successCount: response.successCount,\n failureCount: response.failureCount,\n });\n } catch (error) {\n this.logger.error(\"Multicast send error\", { error, batchSize: batch.length });\n failureCount += batch.length;\n }\n }\n\n // Clean up invalid tokens from database\n if (invalidTokens.length > 0) {\n await this.removeInvalidTokens(invalidTokens);\n }\n\n return { successCount, failureCount, invalidTokens };\n }\n\n /**\n * Remove invalid device tokens from database\n */\n private async removeInvalidTokens(tokens: string[]): Promise<void> {\n try {\n const deleted = await DeviceTokens.destroy({\n where: { deviceToken: tokens },\n });\n\n this.logger.info(\"Removed invalid tokens from database\", {\n count: deleted,\n tokens: tokens.length,\n });\n } catch (error) {\n this.logger.error(\"Failed to remove invalid tokens\", { error });\n }\n }\n\n /**\n * Send notification to specific device tokens\n */\n public async sendToTokens(\n tokens: string[],\n platform: \"ios\" | \"android\",\n notification: NotificationPayload\n ): Promise<SendResult> {\n if (tokens.length === 0) {\n this.logger.warn(\"No tokens provided, skipping notification\");\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n this.logger.info(\"Sending notification to tokens\", {\n tokenCount: tokens.length,\n platform,\n title: notification.title,\n });\n\n return await this.sendMulticast(tokens, notification, platform);\n }\n\n /**\n * Send notification to a single user (all their devices)\n */\n public async sendToUser(params: {\n userUuid: string;\n notification: NotificationPayload;\n }): Promise<SendResult> {\n this.logger.info(\"Sending notification to user\", { userUuid: params.userUuid });\n\n const deviceTokens = await DeviceTokens.findAll({\n where: { userUuid: params.userUuid },\n attributes: [\"deviceToken\", \"platform\"],\n });\n\n if (deviceTokens.length === 0) {\n this.logger.warn(\"No device tokens found for user\", {\n userUuid: params.userUuid,\n });\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\")\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\")\n .map((dt) => dt.deviceToken);\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n return {\n successCount: iosResult.successCount + androidResult.successCount,\n failureCount: iosResult.failureCount + androidResult.failureCount,\n invalidTokens: [...iosResult.invalidTokens, ...androidResult.invalidTokens],\n };\n }\n\n /**\n * Send notification to multiple users\n */\n public async sendToUsers(params: {\n userUuids: string[];\n notification: NotificationPayload;\n }): Promise<SendResult> {\n this.logger.info(\"Sending notification to multiple users\", {\n count: params.userUuids.length,\n });\n\n const deviceTokens = await DeviceTokens.findAll({\n where: { userUuid: params.userUuids },\n attributes: [\"deviceToken\", \"platform\"],\n });\n\n if (deviceTokens.length === 0) {\n this.logger.warn(\"No device tokens found for users\");\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\")\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\")\n .map((dt) => dt.deviceToken);\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n return {\n successCount: iosResult.successCount + androidResult.successCount,\n failureCount: iosResult.failureCount + androidResult.failureCount,\n invalidTokens: [...iosResult.invalidTokens, ...androidResult.invalidTokens],\n };\n }\n\n /**\n * Subscribe users to a topic for efficient group messaging\n */\n public async subscribeToTopic(\n tokens: string[],\n topic: string\n ): Promise<{ successCount: number; failureCount: number }> {\n if (tokens.length === 0) {\n return { successCount: 0, failureCount: 0 };\n }\n\n this.logger.info(\"Subscribing tokens to topic\", {\n tokenCount: tokens.length,\n topic,\n });\n\n try {\n const response = await admin.messaging().subscribeToTopic(tokens, topic);\n\n this.logger.info(\"Topic subscription complete\", {\n successCount: response.successCount,\n failureCount: response.failureCount,\n topic,\n });\n\n return {\n successCount: response.successCount,\n failureCount: response.failureCount,\n };\n } catch (error) {\n this.logger.error(\"Topic subscription error\", { error, topic });\n throw new ExternalAPIError(\n \"Failed to subscribe to topic\",\n `Topic: ${topic}, Error: ${String(error)}`\n );\n }\n }\n\n /**\n * Send notification to a topic (efficient for large groups)\n */\n public async sendToTopic(params: {\n topic: string;\n notification: NotificationPayload;\n }): Promise<{ messageId: string }> {\n this.logger.info(\"Sending notification to topic\", { topic: params.topic });\n\n const message: admin.messaging.Message = {\n topic: params.topic,\n notification: {\n title: params.notification.title,\n body: params.notification.body,\n ...(params.notification.imageUrl && {\n imageUrl: params.notification.imageUrl,\n }),\n },\n data: params.notification.data || {},\n apns: {\n payload: {\n aps: {\n sound: params.notification.sound || \"default\",\n ...(params.notification.badge !== undefined && { badge: params.notification.badge }),\n contentAvailable: true,\n },\n },\n },\n android: {\n priority: \"high\",\n notification: {\n sound: params.notification.sound || \"default\",\n channelId: \"default\",\n },\n },\n };\n\n try {\n const messageId = await admin.messaging().send(message);\n\n this.logger.info(\"Topic notification sent\", {\n topic: params.topic,\n messageId,\n });\n\n return { messageId };\n } catch (error) {\n this.logger.error(\"Topic send error\", { error, topic: params.topic });\n throw new ExternalAPIError(\n \"Failed to send topic notification\",\n `Topic: ${params.topic}, Error: ${String(error)}`\n );\n }\n }\n\n /**\n * Send notification using a template\n */\n public async sendTemplateToUser(params: {\n userUuid: string;\n notificationType: string;\n language?: string;\n data?: Record<string, string>;\n }): Promise<SendResult> {\n this.logger.info(\"Sending template notification to user\", {\n userUuid: params.userUuid,\n notificationType: params.notificationType,\n language: params.language,\n });\n\n const includeOptions: any = {\n model: TranslatedNotification,\n as: \"translations\",\n required: false,\n };\n\n if (params.language) {\n includeOptions.where = { language: params.language };\n }\n\n const template = await NotificationModels.findOne({\n where: { type: params.notificationType as any },\n include: [includeOptions],\n });\n\n if (!template) {\n throw new ExternalAPIError(\n \"Notification template not found\",\n `Type: ${params.notificationType}`\n );\n }\n\n const translation = template.translations?.find(\n (t) => t.language === params.language\n );\n let text = translation?.text || template.text;\n\n if (params.data) {\n Object.entries(params.data).forEach(([key, value]) => {\n text = text.replace(new RegExp(`{{${key}}}`, \"g\"), value);\n });\n }\n\n return await this.sendToUser({\n userUuid: params.userUuid,\n notification: {\n title: params.notificationType,\n body: text,\n ...(params.data && { data: params.data }),\n },\n });\n }\n\n /**\n * Send templated notification to multiple users\n */\n public async sendTemplateToUsers(params: {\n userUuids: string[];\n notificationType: string;\n language?: string;\n data?: Record<string, string>;\n }): Promise<SendResult> {\n this.logger.info(\"Sending template notification to multiple users\", {\n count: params.userUuids.length,\n notificationType: params.notificationType,\n });\n\n const includeOptions: any = {\n model: TranslatedNotification,\n as: \"translations\",\n required: false,\n };\n\n if (params.language) {\n includeOptions.where = { language: params.language };\n }\n\n const template = await NotificationModels.findOne({\n where: { type: params.notificationType as any },\n include: [includeOptions],\n });\n\n if (!template) {\n throw new ExternalAPIError(\n \"Notification template not found\",\n `Type: ${params.notificationType}`\n );\n }\n\n const translation = template.translations?.find(\n (t) => t.language === params.language\n );\n let text = translation?.text || template.text;\n\n if (params.data) {\n Object.entries(params.data).forEach(([key, value]) => {\n text = text.replace(new RegExp(`{{${key}}}`, \"g\"), value);\n });\n }\n\n return await this.sendToUsers({\n userUuids: params.userUuids,\n notification: {\n title: params.notificationType,\n body: text,\n ...(params.data && { data: params.data }),\n },\n });\n }\n\n /**\n * Send notification to all users (uses topic for efficiency)\n * Consider subscribing users to an \"all_users\" topic for better performance\n */\n public async sendToAllUsers(params: {\n notification: NotificationPayload;\n batchSize?: number;\n }): Promise<{ totalSent: number }> {\n const batchSize = params.batchSize || 1000;\n let offset = 0;\n let totalSent = 0;\n\n this.logger.info(\"Starting broadcast notification\", { batchSize });\n\n while (true) {\n const deviceTokens = await DeviceTokens.findAll({\n limit: batchSize,\n offset,\n attributes: [\"deviceToken\", \"platform\"],\n });\n\n if (deviceTokens.length === 0) {\n break;\n }\n\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\")\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\")\n .map((dt) => dt.deviceToken);\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n totalSent += iosResult.successCount + androidResult.successCount;\n offset += batchSize;\n\n this.logger.info(\"Broadcast batch sent\", {\n batchSent: iosResult.successCount + androidResult.successCount,\n totalSent,\n });\n }\n\n this.logger.info(\"Broadcast notification complete\", { totalSent });\n return { totalSent };\n }\n\n /**\n * Send notification to a user group\n * Recommended: Use topics instead for better performance\n */\n public async sendToGroup(params: {\n notification: NotificationPayload;\n group: \"premium\" | \"free\" | \"trial\";\n batchSize?: number;\n }): Promise<{ totalSent: number }> {\n const batchSize = params.batchSize || 1000;\n let offset = 0;\n let totalSent = 0;\n\n this.logger.info(\"Starting group notification\", {\n group: params.group,\n batchSize,\n });\n\n while (true) {\n const users = await PersistedUser.findAll({\n where: { subscriptionType: params.group } as any,\n include: [\n {\n model: DeviceTokens,\n as: \"deviceTokens\",\n attributes: [\"deviceToken\", \"platform\"],\n },\n ],\n limit: batchSize,\n offset,\n });\n\n if (users.length === 0) {\n break;\n }\n\n const allTokens = users.flatMap((user) => (user as any).deviceTokens || []);\n const iosTokens = allTokens\n .filter((dt: any) => dt.platform === \"ios\")\n .map((dt: any) => dt.deviceToken);\n const androidTokens = allTokens\n .filter((dt: any) => dt.platform === \"android\")\n .map((dt: any) => dt.deviceToken);\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n totalSent += iosResult.successCount + androidResult.successCount;\n offset += batchSize;\n\n this.logger.info(\"Group batch sent\", {\n batchSent: iosResult.successCount + androidResult.successCount,\n totalSent,\n });\n }\n\n this.logger.info(\"Group notification complete\", {\n totalSent,\n group: params.group,\n });\n return { totalSent };\n }\n}\n"],"names":["admin","CommonSchemas","ExternalAPIError","Log","DeviceTokens","PersistedUser","NotificationModels","TranslatedNotification","FirebasePushNotificationClient","instance","config","logger","getInstance","extend","app","initializeApp","pushNotification","parse","process","env","resetInstance","shutdown","undefined","apps","length","info","projectId","FCM_PROJECT_ID","credential","cert","clientEmail","FCM_CLIENT_EMAIL","privateKey","FCM_PRIVATE_KEY","error","String","delete","catch","sendMulticast","tokens","notification","platform","successCount","failureCount","invalidTokens","messaging","BATCH_SIZE","i","batch","slice","message","title","body","imageUrl","data","apns","payload","aps","sound","badge","contentAvailable","fcmOptions","android","priority","channelId","response","sendEachForMulticast","responses","forEach","resp","idx","success","errorCode","code","push","warn","token","batchSize","removeInvalidTokens","deleted","destroy","where","deviceToken","count","sendToTokens","tokenCount","sendToUser","params","userUuid","deviceTokens","findAll","attributes","iosTokens","filter","dt","map","androidTokens","iosResult","androidResult","Promise","all","resolve","sendToUsers","userUuids","subscribeToTopic","topic","sendToTopic","messageId","send","sendTemplateToUser","notificationType","language","includeOptions","model","as","required","template","findOne","type","include","translation","translations","find","t","text","Object","entries","key","value","replace","RegExp","sendTemplateToUsers","sendToAllUsers","offset","totalSent","limit","batchSent","sendToGroup","group","users","subscriptionType","allTokens","flatMap","user"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ export declare class CoachExerciseNote extends Model {
|
|
|
6
6
|
coachUuid: string;
|
|
7
7
|
exerciseModelUuid: string;
|
|
8
8
|
note: string;
|
|
9
|
-
|
|
9
|
+
exerciseNoteAuthor: PersistedUser;
|
|
10
10
|
exerciseModel: ExercisesModels;
|
|
11
11
|
}
|
|
12
12
|
//# sourceMappingURL=CoachExerciseNote.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoachExerciseNote.d.ts","sourceRoot":"","sources":["../../../../src/lib/dbmodels/program/CoachExerciseNote.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,EAEN,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,qBAUa,iBAAkB,SAAQ,KAAK;IAMlC,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,EAAE,MAAM,CAAC;IAIlB,iBAAiB,EAAE,MAAM,CAAC;IAG1B,IAAI,EAAE,MAAM,CAAC;IAGb,
|
|
1
|
+
{"version":3,"file":"CoachExerciseNote.d.ts","sourceRoot":"","sources":["../../../../src/lib/dbmodels/program/CoachExerciseNote.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,EAEN,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,qBAUa,iBAAkB,SAAQ,KAAK;IAMlC,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,EAAE,MAAM,CAAC;IAIlB,iBAAiB,EAAE,MAAM,CAAC;IAG1B,IAAI,EAAE,MAAM,CAAC;IAGb,kBAAkB,EAAE,aAAa,CAAC;IAGlC,aAAa,EAAE,eAAe,CAAC;CACxC"}
|
|
@@ -38,7 +38,7 @@ _ts_decorate([
|
|
|
38
38
|
], CoachExerciseNote.prototype, "note", void 0);
|
|
39
39
|
_ts_decorate([
|
|
40
40
|
BelongsTo(()=>PersistedUser, 'coachUuid')
|
|
41
|
-
], CoachExerciseNote.prototype, "
|
|
41
|
+
], CoachExerciseNote.prototype, "exerciseNoteAuthor", void 0);
|
|
42
42
|
_ts_decorate([
|
|
43
43
|
BelongsTo(()=>ExercisesModels, 'exerciseModelUuid')
|
|
44
44
|
], CoachExerciseNote.prototype, "exerciseModel", void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/dbmodels/program/CoachExerciseNote.ts"],"sourcesContent":["import {\n BelongsTo,\n Column,\n DataType,\n Default,\n ForeignKey,\n Model,\n Table,\n} from 'sequelize-typescript';\nimport { ExercisesModels } from './ExerciseModels.js';\nimport { PersistedUser } from '../user/PersistedUser.js';\n\n@Table({\n tableName: 'CoachExerciseNotes',\n indexes: [\n {\n unique: true,\n fields: ['coachUuid', 'exerciseModelUuid'],\n name: 'unique_coach_exercise_note',\n },\n ],\n})\nexport class CoachExerciseNote extends Model {\n @Default(DataType.UUIDV4)\n @Column({\n type: DataType.UUID,\n primaryKey: true,\n })\n declare uuid: string;\n\n @ForeignKey(() => PersistedUser)\n @Column({ type: DataType.UUID, allowNull: false })\n declare coachUuid: string;\n\n @ForeignKey(() => ExercisesModels)\n @Column({ type: DataType.UUID, allowNull: false })\n declare exerciseModelUuid: string;\n\n @Column({ type: DataType.TEXT, allowNull: false })\n declare note: string;\n\n @BelongsTo(() => PersistedUser, 'coachUuid')\n declare
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/dbmodels/program/CoachExerciseNote.ts"],"sourcesContent":["import {\n BelongsTo,\n Column,\n DataType,\n Default,\n ForeignKey,\n Model,\n Table,\n} from 'sequelize-typescript';\nimport { ExercisesModels } from './ExerciseModels.js';\nimport { PersistedUser } from '../user/PersistedUser.js';\n\n@Table({\n tableName: 'CoachExerciseNotes',\n indexes: [\n {\n unique: true,\n fields: ['coachUuid', 'exerciseModelUuid'],\n name: 'unique_coach_exercise_note',\n },\n ],\n})\nexport class CoachExerciseNote extends Model {\n @Default(DataType.UUIDV4)\n @Column({\n type: DataType.UUID,\n primaryKey: true,\n })\n declare uuid: string;\n\n @ForeignKey(() => PersistedUser)\n @Column({ type: DataType.UUID, allowNull: false })\n declare coachUuid: string;\n\n @ForeignKey(() => ExercisesModels)\n @Column({ type: DataType.UUID, allowNull: false })\n declare exerciseModelUuid: string;\n\n @Column({ type: DataType.TEXT, allowNull: false })\n declare note: string;\n\n @BelongsTo(() => PersistedUser, 'coachUuid')\n declare exerciseNoteAuthor: PersistedUser;\n\n @BelongsTo(() => ExercisesModels, 'exerciseModelUuid')\n declare exerciseModel: ExercisesModels;\n}\n"],"names":["BelongsTo","Column","DataType","Default","ForeignKey","Model","Table","ExercisesModels","PersistedUser","CoachExerciseNote","UUIDV4","type","UUID","primaryKey","allowNull","TEXT","tableName","indexes","unique","fields","name"],"mappings":";;;;;;AAAA,SACEA,SAAS,EACTC,MAAM,EACNC,QAAQ,EACRC,OAAO,EACPC,UAAU,EACVC,KAAK,EACLC,KAAK,QACA,uBAAuB;AAC9B,SAASC,eAAe,QAAQ,sBAAsB;AACtD,SAASC,aAAa,QAAQ,2BAA2B;AAYzD,OAAO,MAAMC,0BAA0BJ;AAwBvC;;qBAvBoBK;;QAEhBC,MAAMT,SAASU,IAAI;QACnBC,YAAY;;;;mBAIIL;;QACRG,MAAMT,SAASU,IAAI;QAAEE,WAAW;;;;mBAGxBP;;QACRI,MAAMT,SAASU,IAAI;QAAEE,WAAW;;;;;QAGhCH,MAAMT,SAASa,IAAI;QAAED,WAAW;;;;kBAGzBN;;;kBAGAD;;;;QA/BjBS,WAAW;QACXC,SAAS;YACP;gBACEC,QAAQ;gBACRC,QAAQ;oBAAC;oBAAa;iBAAoB;gBAC1CC,MAAM;YACR;SACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "90dc-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"debug": "^4.4.3",
|
|
41
41
|
"dotenv": "^17.2.3",
|
|
42
42
|
"eslint": "^9.39.1",
|
|
43
|
+
"firebase-admin": "^13.6.1",
|
|
43
44
|
"google-auth-library": "^10.5.0",
|
|
44
45
|
"googleapis": "^165.0.0",
|
|
45
46
|
"ioredis": "^5.8.2",
|