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 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";
@@ -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;AAQhF,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"}
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
- coachUser: PersistedUser;
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,SAAS,EAAE,aAAa,CAAC;IAGzB,aAAa,EAAE,eAAe,CAAC;CACxC"}
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, "coachUser", void 0);
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 coachUser: 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"}
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.15.9",
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",