90dc-core 1.16.13 → 1.16.14

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
@@ -15,7 +15,7 @@ export { NotificationClient } from "./lib/utils/NotificationClient.js";
15
15
  export { Log } from "./lib/utils/Logger.js";
16
16
  export { SecretManager } from "./lib/utils/SecretManager.js";
17
17
  export { initializeSentry, isSentryEnabled, scrubObject, captureRequestBody, extractUserContext, buildRequestContext, buildResponseContext, reportErrorToSentry, reportMessageToSentry, type ErrorReportOptions, type SeverityLevel, } from "./lib/utils/SentryUtil.js";
18
- export { createMockContext, createMockUser, createAuthenticatedContext, mockDatabase, flushPromises, assertions, executeRoute, type BaseContext, type AuthenticatedContext, type RouterContext, type RouterMiddleware, type RouterLayer, type RouterLike, type SequelizeModelMethods, } from "./lib/testing/testHelpers.js";
18
+ export { createMockContext, createMockUser, createAuthenticatedContext, mockDatabase, setupDatabaseMocks, flushPromises, assertions, executeRoute, createMockFile, createMockFiles, createMockTransaction, setupTimeMocks, MockValidationError, MockNotFoundError, MockDatabaseError, createTypedMock, spyOnPrototype, TEST_CONSTANTS, type BaseContext, type AuthenticatedContext, type RouterContext, type RouterMiddleware, type RouterLayer, type RouterLike, type SequelizeModelMethods, type MockFile, type MockTransaction, type DatabaseMocks, type TimeMocks, } from "./lib/testing/testHelpers.js";
19
19
  export { ConfigValidator, BaseConfigSchema, CommonSchemas, createConfig, ConfigurationError, type BaseConfig } from "./lib/config/ConfigValidator.js";
20
20
  export { AppError, ValidationError, AuthenticationError, ForbiddenError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError, InternalServerError, ServiceUnavailableError, DatabaseError, ExternalAPIError, isAppError, isOperationalError, toAppError } from "./lib/Errors/AppError.js";
21
21
  export { ErrorMiddleware, } from "./lib/middlewares/ErrorMiddleware.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,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"}
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,EAEL,iBAAiB,EACjB,cAAc,EACd,0BAA0B,EAE1B,YAAY,EACZ,kBAAkB,EAElB,aAAa,EAEb,UAAU,EAEV,YAAY,EAEZ,cAAc,EACd,eAAe,EACf,qBAAqB,EAErB,cAAc,EAEd,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EAEjB,eAAe,EACf,cAAc,EAEd,cAAc,EAEd,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,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
@@ -18,7 +18,17 @@ export { Log } from "./lib/utils/Logger.js";
18
18
  export { SecretManager } from "./lib/utils/SecretManager.js";
19
19
  export { initializeSentry, isSentryEnabled, scrubObject, captureRequestBody, extractUserContext, buildRequestContext, buildResponseContext, reportErrorToSentry, reportMessageToSentry } from "./lib/utils/SentryUtil.js";
20
20
  //Testing Utilities
21
- export { createMockContext, createMockUser, createAuthenticatedContext, mockDatabase, flushPromises, assertions, executeRoute } from "./lib/testing/testHelpers.js";
21
+ export { // Context helpers
22
+ createMockContext, createMockUser, createAuthenticatedContext, // Database mocks
23
+ mockDatabase, setupDatabaseMocks, // Async helpers
24
+ flushPromises, // Assertions
25
+ assertions, // Route execution
26
+ executeRoute, // Factory helpers
27
+ createMockFile, createMockFiles, createMockTransaction, // Time mocking
28
+ setupTimeMocks, // Mock errors
29
+ MockValidationError, MockNotFoundError, MockDatabaseError, // Typed mocking helpers
30
+ createTypedMock, spyOnPrototype, // Constants
31
+ TEST_CONSTANTS } from "./lib/testing/testHelpers.js";
22
32
  //Config
23
33
  export { ConfigValidator, BaseConfigSchema, CommonSchemas, createConfig, ConfigurationError } from "./lib/config/ConfigValidator.js";
24
34
  //Errors
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 { 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"}
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 // Context helpers\n createMockContext,\n createMockUser,\n createAuthenticatedContext,\n // Database mocks\n mockDatabase,\n setupDatabaseMocks,\n // Async helpers\n flushPromises,\n // Assertions\n assertions,\n // Route execution\n executeRoute,\n // Factory helpers\n createMockFile,\n createMockFiles,\n createMockTransaction,\n // Time mocking\n setupTimeMocks,\n // Mock errors\n MockValidationError,\n MockNotFoundError,\n MockDatabaseError,\n // Typed mocking helpers\n createTypedMock,\n spyOnPrototype,\n // Constants\n TEST_CONSTANTS,\n // Types\n type BaseContext,\n type AuthenticatedContext,\n type RouterContext,\n type RouterMiddleware,\n type RouterLayer,\n type RouterLike,\n type SequelizeModelMethods,\n type MockFile,\n type MockTransaction,\n type DatabaseMocks,\n type TimeMocks,\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","setupDatabaseMocks","flushPromises","assertions","executeRoute","createMockFile","createMockFiles","createMockTransaction","setupTimeMocks","MockValidationError","MockNotFoundError","MockDatabaseError","createTypedMock","spyOnPrototype","TEST_CONSTANTS","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,SACE,kBAAkB;AAClBC,iBAAiB,EACjBC,cAAc,EACdC,0BAA0B,EAC1B,iBAAiB;AACjBC,YAAY,EACZC,kBAAkB,EAClB,gBAAgB;AAChBC,aAAa,EACb,aAAa;AACbC,UAAU,EACV,kBAAkB;AAClBC,YAAY,EACZ,kBAAkB;AAClBC,cAAc,EACdC,eAAe,EACfC,qBAAqB,EACrB,eAAe;AACfC,cAAc,EACd,cAAc;AACdC,mBAAmB,EACnBC,iBAAiB,EACjBC,iBAAiB,EACjB,wBAAwB;AACxBC,eAAe,EACfC,cAAc,EACd,YAAY;AACZC,cAAc,QAaT,+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 +1 @@
1
- {"version":3,"file":"FirebasePushNotificationClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/FirebasePushNotificationClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FirebasePushNotificationClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/FirebasePushNotificationClient.ts"],"names":[],"mappings":"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"}
@@ -13,10 +13,10 @@
13
13
  * - Message delivery tracking
14
14
  * - Type-safe API
15
15
  */ import admin from "firebase-admin";
16
- import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
17
16
  import { CommonSchemas } from "../config/ConfigValidator.js";
18
17
  import { ExternalAPIError } from "../Errors/AppError.js";
19
18
  import { Log } from "../utils/Logger.js";
19
+ import { SecretManager } from "../utils/SecretManager.js";
20
20
  import { DeviceTokens } from "../dbmodels/user/DeviceTokens.js";
21
21
  import { PersistedUser } from "../dbmodels/user/PersistedUser.js";
22
22
  import { NotificationModels } from "../dbmodels/notifications/NotificationModels.js";
@@ -53,7 +53,8 @@ export class FirebasePushNotificationClient {
53
53
  }
54
54
  static async loadServiceAccount() {
55
55
  const logger = Log.getInstance().extend("firebase-push");
56
- const secretName = "projects/1033066542238/secrets/firebase_new_key/versions/latest";
56
+ const PROJECT_ID = "1033066542238";
57
+ const SECRET_NAME = "firebase_new_key";
57
58
  try {
58
59
  // Check if FIREBASE_SERVICE_ACCOUNT_KEY env variable is set (for local development)
59
60
  const localKeyPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
@@ -64,16 +65,13 @@ export class FirebasePushNotificationClient {
64
65
  logger.info("Firebase service account loaded from local file");
65
66
  return serviceAccount;
66
67
  }
67
- // Production: Load from Secret Manager
68
- const client = new SecretManagerServiceClient();
69
- const [version] = await client.accessSecretVersion({
70
- name: secretName
68
+ // Production: Load from Secret Manager using shared utility
69
+ // This provides better error handling, caching, and reuses the client instance
70
+ logger.info("Loading Firebase service account from Secret Manager", {
71
+ projectId: PROJECT_ID,
72
+ secretName: SECRET_NAME
71
73
  });
72
- if (!version.payload?.data) {
73
- throw new Error("Secret payload is empty");
74
- }
75
- const serviceAccountJson = version.payload.data.toString();
76
- const serviceAccount = JSON.parse(serviceAccountJson);
74
+ const serviceAccount = await SecretManager.loadSecretJSON(SECRET_NAME, PROJECT_ID);
77
75
  logger.info("Firebase service account loaded successfully from Secret Manager", {
78
76
  projectId: serviceAccount.project_id,
79
77
  clientEmail: serviceAccount.client_email
@@ -81,9 +79,10 @@ export class FirebasePushNotificationClient {
81
79
  return serviceAccount;
82
80
  } catch (error) {
83
81
  logger.error("Failed to load Firebase service account", {
84
- error
82
+ error: error instanceof Error ? error.message : String(error),
83
+ stack: error instanceof Error ? error.stack : undefined
85
84
  });
86
- throw new ExternalAPIError("Failed to load Firebase credentials", `Error: ${String(error)}`);
85
+ throw new ExternalAPIError("Failed to load Firebase credentials", `Error: ${error instanceof Error ? error.message : String(error)}`);
87
86
  }
88
87
  }
89
88
  static resetInstance() {
@@ -1 +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 admin from \"firebase-admin\";\nimport { z } from \"zod\";\nimport { SecretManagerServiceClient } from \"@google-cloud/secret-manager\";\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\ninterface ServiceAccount {\n type: string;\n project_id: string;\n private_key_id: string;\n private_key: string;\n client_email: string;\n client_id: string;\n auth_uri: string;\n token_uri: string;\n auth_provider_x509_cert_url: string;\n client_x509_cert_url: string;\n}\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 static initPromise: Promise<FirebasePushNotificationClient> | null = null;\n private logger = Log.getInstance().extend(\"firebase-push\");\n private app: admin.app.App | null = null;\n private serviceAccount: ServiceAccount | null = null;\n\n private constructor(_config: FirebaseConfig, serviceAccount: ServiceAccount) {\n // config parameter kept for backward compatibility but not used (FCM config comes from Secret Manager)\n this.serviceAccount = serviceAccount;\n this.initializeApp();\n }\n\n public static async getInstance(): Promise<FirebasePushNotificationClient> {\n if (FirebasePushNotificationClient.instance) {\n return FirebasePushNotificationClient.instance;\n }\n\n // If already initializing, wait for that to complete\n if (FirebasePushNotificationClient.initPromise) {\n return FirebasePushNotificationClient.initPromise;\n }\n\n // Start initialization\n FirebasePushNotificationClient.initPromise = (async () => {\n // Parse config (mostly for optional APNs fields, FCM comes from Secret Manager)\n const parsedConfig = CommonSchemas.pushNotification.parse(process.env);\n const serviceAccount = await FirebasePushNotificationClient.loadServiceAccount();\n FirebasePushNotificationClient.instance = new FirebasePushNotificationClient(parsedConfig, serviceAccount);\n FirebasePushNotificationClient.initPromise = null;\n return FirebasePushNotificationClient.instance;\n })();\n\n return FirebasePushNotificationClient.initPromise;\n }\n\n private static async loadServiceAccount(): Promise<ServiceAccount> {\n const logger = Log.getInstance().extend(\"firebase-push\");\n const secretName = \"projects/1033066542238/secrets/firebase_new_key/versions/latest\";\n\n try {\n // Check if FIREBASE_SERVICE_ACCOUNT_KEY env variable is set (for local development)\n const localKeyPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;\n if (localKeyPath) {\n const fs = await import(\"fs/promises\");\n const serviceAccountJson = await fs.readFile(localKeyPath, \"utf-8\");\n const serviceAccount = JSON.parse(serviceAccountJson) as ServiceAccount;\n\n logger.info(\"Firebase service account loaded from local file\");\n return serviceAccount;\n }\n\n // Production: Load from Secret Manager\n const client = new SecretManagerServiceClient();\n const [version] = await client.accessSecretVersion({ name: secretName });\n\n if (!version.payload?.data) {\n throw new Error(\"Secret payload is empty\");\n }\n\n const serviceAccountJson = version.payload.data.toString();\n const serviceAccount = JSON.parse(serviceAccountJson) as ServiceAccount;\n\n logger.info(\"Firebase service account loaded successfully from Secret Manager\", {\n projectId: serviceAccount.project_id,\n clientEmail: serviceAccount.client_email,\n });\n return serviceAccount;\n } catch (error) {\n logger.error(\"Failed to load Firebase service account\", { error });\n throw new ExternalAPIError(\n \"Failed to load Firebase credentials\",\n `Error: ${String(error)}`\n );\n }\n }\n\n public static resetInstance(): void {\n FirebasePushNotificationClient.instance?.shutdown();\n FirebasePushNotificationClient.instance = undefined as any;\n FirebasePushNotificationClient.initPromise = null;\n }\n\n private initializeApp(): void {\n try {\n // Check if app already exists\n if (admin.apps?.length && admin.apps.length > 0) {\n this.app = admin.app();\n this.logger.info(\"Using existing Firebase app\");\n return;\n }\n\n if (!this.serviceAccount) {\n throw new Error(\"Service account not loaded\");\n }\n\n // Validate service account has all required fields\n if (!this.serviceAccount.project_id || !this.serviceAccount.client_email || !this.serviceAccount.private_key) {\n throw new Error(\"Invalid service account: missing required fields\");\n }\n\n this.logger.info(\"Initializing Firebase Admin SDK\", {\n projectId: this.serviceAccount.project_id,\n clientEmail: this.serviceAccount.client_email,\n });\n\n this.app = admin.initializeApp({\n credential: admin.credential.cert(this.serviceAccount as admin.ServiceAccount),\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 * Detect if a token is a legacy APN token (hex format, 64 characters)\n */\n private isLegacyAPNToken(token: string): boolean {\n // Legacy APN tokens are 64 hex characters (sometimes with spaces or angle brackets)\n const cleanToken = token.replace(/[<>\\s]/g, '');\n return /^[0-9a-fA-F]{64}$/.test(cleanToken);\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 // Detect if any tokens are legacy APN tokens\n const hasLegacyAPNTokens = tokens.some(token => this.isLegacyAPNToken(token));\n const isIOSPlatform = platform === \"ios\" || hasLegacyAPNTokens;\n\n if (hasLegacyAPNTokens) {\n this.logger.info(\"Detected legacy APN tokens in batch\", {\n totalTokens: tokens.length,\n legacyCount: tokens.filter(t => this.isLegacyAPNToken(t)).length,\n });\n }\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 ...(isIOSPlatform && {\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 // Separate tokens by platform, also detecting legacy APN tokens\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\" || this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\" && !this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n\n const legacyAPNCount = deviceTokens.filter(dt => this.isLegacyAPNToken(dt.deviceToken)).length;\n if (legacyAPNCount > 0) {\n this.logger.info(\"Found legacy APN tokens for user\", {\n userUuid: params.userUuid,\n legacyTokenCount: legacyAPNCount,\n });\n }\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 // Separate tokens by platform, also detecting legacy APN tokens\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\" || this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\" && !this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n\n const legacyAPNCount = deviceTokens.filter(dt => this.isLegacyAPNToken(dt.deviceToken)).length;\n if (legacyAPNCount > 0) {\n this.logger.info(\"Found legacy APN tokens for users\", {\n legacyTokenCount: legacyAPNCount,\n });\n }\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","SecretManagerServiceClient","CommonSchemas","ExternalAPIError","Log","DeviceTokens","PersistedUser","NotificationModels","TranslatedNotification","FirebasePushNotificationClient","instance","initPromise","logger","getInstance","extend","app","serviceAccount","_config","initializeApp","parsedConfig","pushNotification","parse","process","env","loadServiceAccount","secretName","localKeyPath","FIREBASE_SERVICE_ACCOUNT_KEY","fs","serviceAccountJson","readFile","JSON","info","client","version","accessSecretVersion","name","payload","data","Error","toString","projectId","project_id","clientEmail","client_email","error","String","resetInstance","shutdown","undefined","apps","length","private_key","credential","cert","delete","catch","isLegacyAPNToken","token","cleanToken","replace","test","sendMulticast","tokens","notification","platform","successCount","failureCount","invalidTokens","messaging","hasLegacyAPNTokens","some","isIOSPlatform","totalTokens","legacyCount","filter","t","BATCH_SIZE","i","batch","slice","message","title","body","imageUrl","apns","aps","sound","badge","contentAvailable","fcmOptions","android","priority","channelId","response","sendEachForMulticast","responses","forEach","resp","idx","success","errorCode","code","push","warn","batchSize","removeInvalidTokens","deleted","destroy","where","deviceToken","count","sendToTokens","tokenCount","sendToUser","params","userUuid","deviceTokens","findAll","attributes","iosTokens","dt","map","androidTokens","legacyAPNCount","legacyTokenCount","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","text","Object","entries","key","value","RegExp","sendTemplateToUsers","sendToAllUsers","offset","totalSent","limit","batchSent","sendToGroup","group","users","subscriptionType","allTokens","flatMap","user"],"mappings":"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"}
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 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 { SecretManager } from \"../utils/SecretManager.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\ninterface ServiceAccount {\n type: string;\n project_id: string;\n private_key_id: string;\n private_key: string;\n client_email: string;\n client_id: string;\n auth_uri: string;\n token_uri: string;\n auth_provider_x509_cert_url: string;\n client_x509_cert_url: string;\n}\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 static initPromise: Promise<FirebasePushNotificationClient> | null = null;\n private logger = Log.getInstance().extend(\"firebase-push\");\n private app: admin.app.App | null = null;\n private serviceAccount: ServiceAccount | null = null;\n\n private constructor(_config: FirebaseConfig, serviceAccount: ServiceAccount) {\n // config parameter kept for backward compatibility but not used (FCM config comes from Secret Manager)\n this.serviceAccount = serviceAccount;\n this.initializeApp();\n }\n\n public static async getInstance(): Promise<FirebasePushNotificationClient> {\n if (FirebasePushNotificationClient.instance) {\n return FirebasePushNotificationClient.instance;\n }\n\n // If already initializing, wait for that to complete\n if (FirebasePushNotificationClient.initPromise) {\n return FirebasePushNotificationClient.initPromise;\n }\n\n // Start initialization\n FirebasePushNotificationClient.initPromise = (async () => {\n // Parse config (mostly for optional APNs fields, FCM comes from Secret Manager)\n const parsedConfig = CommonSchemas.pushNotification.parse(process.env);\n const serviceAccount = await FirebasePushNotificationClient.loadServiceAccount();\n FirebasePushNotificationClient.instance = new FirebasePushNotificationClient(parsedConfig, serviceAccount);\n FirebasePushNotificationClient.initPromise = null;\n return FirebasePushNotificationClient.instance;\n })();\n\n return FirebasePushNotificationClient.initPromise;\n }\n\n private static async loadServiceAccount(): Promise<ServiceAccount> {\n const logger = Log.getInstance().extend(\"firebase-push\");\n const PROJECT_ID = \"1033066542238\";\n const SECRET_NAME = \"firebase_new_key\";\n\n try {\n // Check if FIREBASE_SERVICE_ACCOUNT_KEY env variable is set (for local development)\n const localKeyPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;\n if (localKeyPath) {\n const fs = await import(\"fs/promises\");\n const serviceAccountJson = await fs.readFile(localKeyPath, \"utf-8\");\n const serviceAccount = JSON.parse(serviceAccountJson) as ServiceAccount;\n\n logger.info(\"Firebase service account loaded from local file\");\n return serviceAccount;\n }\n\n // Production: Load from Secret Manager using shared utility\n // This provides better error handling, caching, and reuses the client instance\n logger.info(\"Loading Firebase service account from Secret Manager\", {\n projectId: PROJECT_ID,\n secretName: SECRET_NAME,\n });\n\n const serviceAccount = await SecretManager.loadSecretJSON<ServiceAccount>(\n SECRET_NAME,\n PROJECT_ID\n );\n\n logger.info(\"Firebase service account loaded successfully from Secret Manager\", {\n projectId: serviceAccount.project_id,\n clientEmail: serviceAccount.client_email,\n });\n return serviceAccount;\n } catch (error) {\n logger.error(\"Failed to load Firebase service account\", {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n throw new ExternalAPIError(\n \"Failed to load Firebase credentials\",\n `Error: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n public static resetInstance(): void {\n FirebasePushNotificationClient.instance?.shutdown();\n FirebasePushNotificationClient.instance = undefined as any;\n FirebasePushNotificationClient.initPromise = null;\n }\n\n private initializeApp(): void {\n try {\n // Check if app already exists\n if (admin.apps?.length && admin.apps.length > 0) {\n this.app = admin.app();\n this.logger.info(\"Using existing Firebase app\");\n return;\n }\n\n if (!this.serviceAccount) {\n throw new Error(\"Service account not loaded\");\n }\n\n // Validate service account has all required fields\n if (!this.serviceAccount.project_id || !this.serviceAccount.client_email || !this.serviceAccount.private_key) {\n throw new Error(\"Invalid service account: missing required fields\");\n }\n\n this.logger.info(\"Initializing Firebase Admin SDK\", {\n projectId: this.serviceAccount.project_id,\n clientEmail: this.serviceAccount.client_email,\n });\n\n this.app = admin.initializeApp({\n credential: admin.credential.cert(this.serviceAccount as admin.ServiceAccount),\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 * Detect if a token is a legacy APN token (hex format, 64 characters)\n */\n private isLegacyAPNToken(token: string): boolean {\n // Legacy APN tokens are 64 hex characters (sometimes with spaces or angle brackets)\n const cleanToken = token.replace(/[<>\\s]/g, '');\n return /^[0-9a-fA-F]{64}$/.test(cleanToken);\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 // Detect if any tokens are legacy APN tokens\n const hasLegacyAPNTokens = tokens.some(token => this.isLegacyAPNToken(token));\n const isIOSPlatform = platform === \"ios\" || hasLegacyAPNTokens;\n\n if (hasLegacyAPNTokens) {\n this.logger.info(\"Detected legacy APN tokens in batch\", {\n totalTokens: tokens.length,\n legacyCount: tokens.filter(t => this.isLegacyAPNToken(t)).length,\n });\n }\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 ...(isIOSPlatform && {\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 // Separate tokens by platform, also detecting legacy APN tokens\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\" || this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\" && !this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n\n const legacyAPNCount = deviceTokens.filter(dt => this.isLegacyAPNToken(dt.deviceToken)).length;\n if (legacyAPNCount > 0) {\n this.logger.info(\"Found legacy APN tokens for user\", {\n userUuid: params.userUuid,\n legacyTokenCount: legacyAPNCount,\n });\n }\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 // Separate tokens by platform, also detecting legacy APN tokens\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\" || this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\" && !this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n\n const legacyAPNCount = deviceTokens.filter(dt => this.isLegacyAPNToken(dt.deviceToken)).length;\n if (legacyAPNCount > 0) {\n this.logger.info(\"Found legacy APN tokens for users\", {\n legacyTokenCount: legacyAPNCount,\n });\n }\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","SecretManager","DeviceTokens","PersistedUser","NotificationModels","TranslatedNotification","FirebasePushNotificationClient","instance","initPromise","logger","getInstance","extend","app","serviceAccount","_config","initializeApp","parsedConfig","pushNotification","parse","process","env","loadServiceAccount","PROJECT_ID","SECRET_NAME","localKeyPath","FIREBASE_SERVICE_ACCOUNT_KEY","fs","serviceAccountJson","readFile","JSON","info","projectId","secretName","loadSecretJSON","project_id","clientEmail","client_email","error","Error","message","String","stack","undefined","resetInstance","shutdown","apps","length","private_key","credential","cert","delete","catch","isLegacyAPNToken","token","cleanToken","replace","test","sendMulticast","tokens","notification","platform","successCount","failureCount","invalidTokens","messaging","hasLegacyAPNTokens","some","isIOSPlatform","totalTokens","legacyCount","filter","t","BATCH_SIZE","i","batch","slice","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","batchSize","removeInvalidTokens","deleted","destroy","where","deviceToken","count","sendToTokens","tokenCount","sendToUser","params","userUuid","deviceTokens","findAll","attributes","iosTokens","dt","map","androidTokens","legacyAPNCount","legacyTokenCount","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","text","Object","entries","key","value","RegExp","sendTemplateToUsers","sendToAllUsers","offset","totalSent","limit","batchSent","sendToGroup","group","users","subscriptionType","allTokens","flatMap","user"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { UserTypes } from '../models/UserInterfaces.js';
2
+ export type Mock = any;
2
3
  export interface BaseContext {
3
4
  request: {
4
5
  body?: unknown;
@@ -42,6 +43,131 @@ export declare const mockDatabase: {
42
43
  mockModel<T = unknown>(methods?: Partial<SequelizeModelMethods<T>>): SequelizeModelMethods<T>;
43
44
  };
44
45
  export declare function flushPromises(): Promise<void>;
46
+ /**
47
+ * ============================================================================
48
+ * TEST DATA FACTORIES
49
+ * Following industry best practices from Testing Blueprint
50
+ * ============================================================================
51
+ */
52
+ /**
53
+ * Constants for testing - avoids magic values
54
+ */
55
+ export declare const TEST_CONSTANTS: {
56
+ readonly VALID_UUID: "123e4567-e89b-12d3-a456-426614174000";
57
+ readonly INVALID_UUID: "invalid-uuid";
58
+ readonly VALID_TIMEZONE: "America/New_York";
59
+ readonly INVALID_TIMEZONE: "Invalid/Timezone";
60
+ readonly DEFAULT_TIMEZONE: "UTC";
61
+ readonly TEST_DATE: "2026-02-25T10:00:00Z";
62
+ readonly WEEK_START: "2026-02-23";
63
+ readonly MAX_JSON_SIZE: number;
64
+ };
65
+ /**
66
+ * Mock file upload structure (multer format)
67
+ */
68
+ export interface MockFile {
69
+ fieldname: string;
70
+ originalname: string;
71
+ encoding: string;
72
+ mimetype: string;
73
+ buffer: Buffer;
74
+ size: number;
75
+ }
76
+ /**
77
+ * Creates a mock file for upload testing
78
+ */
79
+ export declare function createMockFile(overrides?: Partial<MockFile>): MockFile;
80
+ /**
81
+ * Creates multiple mock files
82
+ */
83
+ export declare function createMockFiles(count: number): MockFile[];
84
+ /**
85
+ * Mock transaction for database testing
86
+ */
87
+ export interface MockTransaction {
88
+ commit: Mock;
89
+ rollback: Mock;
90
+ finished?: string;
91
+ }
92
+ /**
93
+ * Creates a mock Sequelize transaction
94
+ */
95
+ export declare function createMockTransaction(): MockTransaction;
96
+ /**
97
+ * ============================================================================
98
+ * MOCK CONFIGURATIONS
99
+ * Pre-configured mocks for common testing scenarios
100
+ * ============================================================================
101
+ */
102
+ /**
103
+ * Database mock configuration
104
+ */
105
+ export interface DatabaseMocks {
106
+ findOne: Mock;
107
+ findOrCreate: Mock;
108
+ findByPk: Mock;
109
+ findAll: Mock;
110
+ create: Mock;
111
+ update: Mock;
112
+ upsert: Mock;
113
+ bulkCreate: Mock;
114
+ destroy: Mock;
115
+ reset: () => void;
116
+ restore: () => void;
117
+ }
118
+ /**
119
+ * Creates a set of database operation mocks with reset/restore functionality
120
+ */
121
+ export declare function setupDatabaseMocks(): DatabaseMocks;
122
+ /**
123
+ * Time-related mock utilities
124
+ */
125
+ export interface TimeMocks {
126
+ setTime: (date: string | Date) => void;
127
+ advanceTime: (ms: number) => void;
128
+ restore: () => void;
129
+ }
130
+ /**
131
+ * Sets up fake timers for timezone testing
132
+ * Usage:
133
+ * ```typescript
134
+ * describe('My test', () => {
135
+ * const timeMocks = setupTimeMocks();
136
+ *
137
+ * it('test with fixed time', () => {
138
+ * timeMocks.setTime('2026-02-25T10:00:00Z');
139
+ * // ... test code
140
+ * });
141
+ * });
142
+ * ```
143
+ */
144
+ export declare function setupTimeMocks(): TimeMocks;
145
+ /**
146
+ * Mock error instances for testing error handling
147
+ */
148
+ export declare class MockValidationError extends Error {
149
+ issues: any[];
150
+ constructor(message: string, issues?: any[]);
151
+ }
152
+ export declare class MockNotFoundError extends Error {
153
+ constructor(message: string);
154
+ }
155
+ export declare class MockDatabaseError extends Error {
156
+ constructor(message: string);
157
+ }
158
+ /**
159
+ * Helper to create a typed mock function
160
+ */
161
+ export declare function createTypedMock(): Mock;
162
+ /**
163
+ * Helper to create a typed spy on a prototype method
164
+ * Usage:
165
+ * ```typescript
166
+ * const mockMethod = spyOnPrototype(MyService.prototype, 'methodName');
167
+ * mockMethod.mockResolvedValue({ success: true });
168
+ * ```
169
+ */
170
+ export declare function spyOnPrototype<T, K extends keyof T>(prototype: T, method: K): Mock;
45
171
  export declare const assertions: {
46
172
  assertDefined<T>(value: T | null | undefined, message?: string): asserts value is T;
47
173
  assertStatusCode(ctx: BaseContext | {
@@ -1 +1 @@
1
- {"version":3,"file":"testHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testHelpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAG7D,MAAM,MAAM,IAAI,GAAG,GAAG,CAAC;AAGvB,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE;QACP,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;QACxD,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;IACF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;IACtD,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C;AAGD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACnE,SAAS,GAAE,OAAO,CAAC,CAAC,CAAM,GACzB,CAAC,CA6BH;AAGD,wBAAgB,cAAc,CAAC,SAAS,GAAE,OAAO,CAAC,SAAS,CAAM,GAAG,SAAS,CAW5E;AAGD,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,KAAK,EAAE;QACL,IAAI,EAAE,SAAS,CAAC;QAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAGD,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,WAAW,GAAG,oBAAoB,EACrF,aAAa,GAAE,OAAO,CAAC,SAAS,CAAM,EACtC,gBAAgB,GAAE,OAAO,CAAC,CAAC,CAAM,GAChC,CAAC,CAcH;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC,GAAG,OAAO;IAChD,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;IAChE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC;IAC3D,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;IACtF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC5E,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACxF,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAC9D,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CAC7D;AAGD,eAAO,MAAM,YAAY;cAEb,CAAC,sBACA,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,GACzC,qBAAqB,CAAC,CAAC,CAAC;CAY5B,CAAC;AAGF,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AAED;;;;;GAKG;AAEH;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;;;CASjB,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,GAAE,OAAO,CAAC,QAAQ,CAAM,GAAG,QAAQ,CAU1E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAOzD;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,IAAI,CAAC;IACb,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CASvD;AAED;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,IAAI,CAAC;IACd,YAAY,EAAE,IAAI,CAAC;IACnB,QAAQ,EAAE,IAAI,CAAC;IACf,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,aAAa,CA2BlD;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,IAAI,SAAS,CA4B1C;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAGnC,MAAM,EAAE,GAAG,EAAE;gBADpB,OAAO,EAAE,MAAM,EACR,MAAM,GAAE,GAAG,EAAO;CAK5B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAKtC;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,EACjD,SAAS,EAAE,CAAC,EACZ,MAAM,EAAE,CAAC,GACR,IAAI,CAMN;AAGD,eAAO,MAAM,UAAU;kBAEP,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,YAAY,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAC;0BAO7D,WAAW,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,YAAY,MAAM,GAAG,IAAI;8BASvF,OAAO,gBACC,MAAM,EAAE,GACrB,QAAQ,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;iBAc7B,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,YAAY,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAC;eAMvE,CAAC,SAAS,OAAO,QAAQ,MAAM,YAAY,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAC;6BAMxD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OACnD,OAAO,iBACG,CAAC,MAAM,CAAC,CAAC,EAAE,GACzB,GAAG,IAAI,CAAC;CAMZ,CAAC;AAEF,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAGD,MAAM,MAAM,gBAAgB,CAAC,QAAQ,GAAG,WAAW,IAAI,CACrD,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KACtB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAG1B,MAAM,WAAW,WAAW,CAAC,QAAQ,GAAG,WAAW;IACjD,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAGD,MAAM,WAAW,UAAU,CAAC,QAAQ,GAAG,WAAW;IAChD,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;CAChC;AAED,wBAAsB,YAAY,CAAC,QAAQ,SAAS,WAAW,GAAG,WAAW,EAC3E,MAAM,EAAE;IAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,EAAE,CAAA;CAAE,EACnF,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,IAAI,CAAC,CAyCf"}
@@ -73,6 +73,167 @@ export const mockDatabase = {
73
73
  export async function flushPromises() {
74
74
  return new Promise((resolve)=>setImmediate(resolve));
75
75
  }
76
+ /**
77
+ * ============================================================================
78
+ * TEST DATA FACTORIES
79
+ * Following industry best practices from Testing Blueprint
80
+ * ============================================================================
81
+ */ /**
82
+ * Constants for testing - avoids magic values
83
+ */ export const TEST_CONSTANTS = {
84
+ VALID_UUID: '123e4567-e89b-12d3-a456-426614174000',
85
+ INVALID_UUID: 'invalid-uuid',
86
+ VALID_TIMEZONE: 'America/New_York',
87
+ INVALID_TIMEZONE: 'Invalid/Timezone',
88
+ DEFAULT_TIMEZONE: 'UTC',
89
+ TEST_DATE: '2026-02-25T10:00:00Z',
90
+ WEEK_START: '2026-02-23',
91
+ MAX_JSON_SIZE: 1024 * 1024
92
+ };
93
+ /**
94
+ * Creates a mock file for upload testing
95
+ */ export function createMockFile(overrides = {}) {
96
+ return {
97
+ fieldname: 'photos',
98
+ originalname: 'test-photo.jpg',
99
+ encoding: '7bit',
100
+ mimetype: 'image/jpeg',
101
+ buffer: Buffer.from('fake-image-data'),
102
+ size: 1024,
103
+ ...overrides
104
+ };
105
+ }
106
+ /**
107
+ * Creates multiple mock files
108
+ */ export function createMockFiles(count) {
109
+ return Array.from({
110
+ length: count
111
+ }, (_, i)=>createMockFile({
112
+ originalname: `test-photo-${i}.jpg`,
113
+ size: 1024 * (i + 1)
114
+ }));
115
+ }
116
+ /**
117
+ * Creates a mock Sequelize transaction
118
+ */ export function createMockTransaction() {
119
+ const mockFn = globalThis.jest?.fn || (()=>{
120
+ throw new Error('jest.fn is not available. Make sure jest is properly configured.');
121
+ });
122
+ return {
123
+ commit: mockFn().mockResolvedValue(undefined),
124
+ rollback: mockFn().mockResolvedValue(undefined)
125
+ };
126
+ }
127
+ /**
128
+ * Creates a set of database operation mocks with reset/restore functionality
129
+ */ export function setupDatabaseMocks() {
130
+ // Note: jest must be imported in the test file for this to work
131
+ const mockFn = globalThis.jest?.fn || (()=>{
132
+ throw new Error('jest.fn is not available. Make sure jest is properly configured.');
133
+ });
134
+ const mocks = {
135
+ findOne: mockFn(),
136
+ findOrCreate: mockFn(),
137
+ findByPk: mockFn(),
138
+ findAll: mockFn(),
139
+ create: mockFn(),
140
+ update: mockFn(),
141
+ upsert: mockFn(),
142
+ bulkCreate: mockFn(),
143
+ destroy: mockFn()
144
+ };
145
+ return {
146
+ ...mocks,
147
+ reset: ()=>{
148
+ Object.values(mocks).forEach((mock)=>mock.mockReset?.());
149
+ },
150
+ restore: ()=>{
151
+ Object.values(mocks).forEach((mock)=>mock.mockRestore?.());
152
+ }
153
+ };
154
+ }
155
+ /**
156
+ * Sets up fake timers for timezone testing
157
+ * Usage:
158
+ * ```typescript
159
+ * describe('My test', () => {
160
+ * const timeMocks = setupTimeMocks();
161
+ *
162
+ * it('test with fixed time', () => {
163
+ * timeMocks.setTime('2026-02-25T10:00:00Z');
164
+ * // ... test code
165
+ * });
166
+ * });
167
+ * ```
168
+ */ export function setupTimeMocks() {
169
+ const jestGlobal = globalThis.jest;
170
+ const beforeEachGlobal = globalThis.beforeEach;
171
+ const afterEachGlobal = globalThis.afterEach;
172
+ if (beforeEachGlobal) {
173
+ beforeEachGlobal(()=>{
174
+ jestGlobal?.useFakeTimers();
175
+ });
176
+ }
177
+ if (afterEachGlobal) {
178
+ afterEachGlobal(()=>{
179
+ jestGlobal?.useRealTimers();
180
+ });
181
+ }
182
+ return {
183
+ setTime: (date)=>{
184
+ jestGlobal?.setSystemTime(new Date(date));
185
+ },
186
+ advanceTime: (ms)=>{
187
+ jestGlobal?.advanceTimersByTime(ms);
188
+ },
189
+ restore: ()=>{
190
+ jestGlobal?.useRealTimers();
191
+ }
192
+ };
193
+ }
194
+ /**
195
+ * Mock error instances for testing error handling
196
+ */ export class MockValidationError extends Error {
197
+ issues;
198
+ constructor(message, issues = []){
199
+ super(message), this.issues = issues;
200
+ this.name = 'ValidationError';
201
+ }
202
+ }
203
+ export class MockNotFoundError extends Error {
204
+ constructor(message){
205
+ super(message);
206
+ this.name = 'NotFoundError';
207
+ }
208
+ }
209
+ export class MockDatabaseError extends Error {
210
+ constructor(message){
211
+ super(message);
212
+ this.name = 'SequelizeDatabaseError';
213
+ }
214
+ }
215
+ /**
216
+ * Helper to create a typed mock function
217
+ */ export function createTypedMock() {
218
+ const mockFn = globalThis.jest?.fn || (()=>{
219
+ throw new Error('jest.fn is not available. Make sure jest is properly configured.');
220
+ });
221
+ return mockFn();
222
+ }
223
+ /**
224
+ * Helper to create a typed spy on a prototype method
225
+ * Usage:
226
+ * ```typescript
227
+ * const mockMethod = spyOnPrototype(MyService.prototype, 'methodName');
228
+ * mockMethod.mockResolvedValue({ success: true });
229
+ * ```
230
+ */ export function spyOnPrototype(prototype, method) {
231
+ const jestGlobal = globalThis.jest;
232
+ if (!jestGlobal?.spyOn) {
233
+ throw new Error('jest.spyOn is not available. Make sure jest is properly configured.');
234
+ }
235
+ return jestGlobal.spyOn(prototype, method);
236
+ }
76
237
  export const assertions = {
77
238
  assertDefined (value, message) {
78
239
  if (value === null || value === undefined) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/testing/testHelpers.ts"],"sourcesContent":["import type { UserTypes } from '../models/UserInterfaces.js';\n\n\nexport interface BaseContext {\n request: {\n body?: unknown;\n headers?: Record<string, string | string[] | undefined>;\n files?: unknown;\n };\n response: {\n status: number;\n body?: unknown;\n };\n params?: Record<string, string>;\n query?: Record<string, string | string[] | undefined>;\n state: Record<string, unknown>;\n status: number;\n body?: unknown;\n headers: Record<string, string | string[] | undefined>;\n method?: string;\n path?: string;\n get: (field: string) => string | string[] | undefined;\n set: (field: string, value: string) => void;\n}\n\n\nexport function createMockContext<T extends BaseContext = BaseContext>(\n overrides: Partial<T> = {},\n): T {\n const ctx: BaseContext = {\n request: {\n body: {},\n headers: {},\n ...overrides.request,\n },\n response: {\n status: 200,\n body: undefined,\n ...overrides.response,\n },\n params: overrides.params || {},\n query: overrides.query || {},\n state: overrides.state || {},\n status: overrides.status || 200,\n body: overrides.body,\n headers: overrides.headers || {},\n method: overrides.method || 'GET',\n path: overrides.path || '/',\n get: (field: string) => {\n return ctx.headers[field.toLowerCase()];\n },\n set: (field: string, value: string) => {\n ctx.headers[field.toLowerCase()] = value;\n },\n };\n\n return { ...ctx, ...overrides } as T;\n}\n\n\nexport function createMockUser(overrides: Partial<UserTypes> = {}): UserTypes {\n return {\n userUuid: overrides.userUuid || 'test-user-uuid',\n email: overrides.email || 'test@example.com',\n firstName: overrides.firstName || 'Test',\n lastName: overrides.lastName || 'User',\n avatar: overrides.avatar || '1',\n role: overrides.role || 'user',\n isSubscriptionActive: overrides.isSubscriptionActive ?? true,\n ...overrides,\n };\n}\n\n\nexport interface AuthenticatedContext extends BaseContext {\n state: {\n user: UserTypes;\n [key: string]: unknown;\n };\n}\n\n\nexport function createAuthenticatedContext<T extends BaseContext = AuthenticatedContext>(\n userOverrides: Partial<UserTypes> = {},\n contextOverrides: Partial<T> = {},\n): T {\n const user = createMockUser(userOverrides);\n\n return createMockContext<T>({\n ...contextOverrides,\n state: {\n user,\n ...(contextOverrides.state || {}),\n },\n headers: {\n authorization: 'Bearer mock-token',\n ...(contextOverrides.headers || {}),\n },\n } as Partial<T>);\n}\n\nexport interface SequelizeModelMethods<T = unknown> {\n findOne: ((options?: unknown) => Promise<T | null>) | undefined;\n findAll: ((options?: unknown) => Promise<T[]>) | undefined;\n findByPk: ((id: string | number, options?: unknown) => Promise<T | null>) | undefined;\n create: ((values: Partial<T>, options?: unknown) => Promise<T>) | undefined;\n update: ((values: Partial<T>, options?: unknown) => Promise<[number, T[]]>) | undefined;\n destroy: ((options?: unknown) => Promise<number>) | undefined;\n count: ((options?: unknown) => Promise<number>) | undefined;\n}\n\n\nexport const mockDatabase = {\n\n mockModel<T = unknown>(\n methods: Partial<SequelizeModelMethods<T>> = {},\n ): SequelizeModelMethods<T> {\n return {\n findOne: undefined,\n findAll: undefined,\n findByPk: undefined,\n create: undefined,\n update: undefined,\n destroy: undefined,\n count: undefined,\n ...methods,\n };\n },\n};\n\n\nexport async function flushPromises(): Promise<void> {\n return new Promise((resolve) => setImmediate(resolve));\n}\n\n\nexport const assertions = {\n\n assertDefined<T>(value: T | null | undefined, message?: string): asserts value is T {\n if (value === null || value === undefined) {\n throw new Error(message || 'Expected value to be defined');\n }\n },\n\n\n assertStatusCode(ctx: BaseContext | { status: number; body?: unknown }, expected: number): void {\n if (ctx.status !== expected) {\n throw new Error(\n `Expected status ${expected} but got ${ctx.status}. Body: ${JSON.stringify(ctx.body)}`,\n );\n }\n },\n\n assertResponseShape(\n body: unknown,\n expectedKeys: string[],\n ): asserts body is Record<string, unknown> {\n if (typeof body !== 'object' || body === null) {\n throw new Error('Expected body to be an object');\n }\n const actualKeys = Object.keys(body);\n const missingKeys = expectedKeys.filter((key) => !actualKeys.includes(key));\n if (missingKeys.length > 0) {\n throw new Error(\n `Missing expected keys in response: ${missingKeys.join(', ')}. Got: ${actualKeys.join(', ')}`,\n );\n }\n },\n\n\n assertExists<T>(value: T | null | undefined, message?: string): asserts value is T {\n if (value === null || value === undefined) {\n throw new Error(message || 'Expected value to exist');\n }\n },\n\n assertType<T>(value: unknown, type: string, message?: string): asserts value is T {\n if (typeof value !== type) {\n throw new Error(message || `Expected value to be of type ${type}, got ${typeof value}`);\n }\n },\n\n validateObjectProperties<T extends Record<string, unknown>>(\n obj: unknown,\n expectedProps: (keyof T)[],\n ): obj is T {\n if (typeof obj !== 'object' || obj === null) {\n return false;\n }\n return expectedProps.every((prop) => prop in obj);\n },\n};\n\nexport interface RouterContext extends BaseContext {\n params: Record<string, string>;\n router?: unknown;\n _matchedRoute?: string;\n _matchedRouteName?: string;\n}\n\n\nexport type RouterMiddleware<TContext = BaseContext> = (\n ctx: TContext,\n next: () => Promise<void>\n) => Promise<void> | void;\n\n\nexport interface RouterLayer<TContext = BaseContext> {\n path: string | RegExp;\n methods: string[];\n stack: RouterMiddleware<TContext>[];\n name?: string | null;\n}\n\n\nexport interface RouterLike<TContext = BaseContext> {\n stack: RouterLayer<TContext>[];\n}\n\nexport async function executeRoute<TContext extends BaseContext = BaseContext>(\n router: { stack: { path: string | RegExp; methods: string[]; stack: unknown[] }[] },\n path: string,\n method: string,\n ctx: TContext,\n): Promise<void> {\n const route = router.stack.find(\n (layer) => layer.path === path && layer.methods.includes(method)\n );\n\n if (!route) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Cast context to include router properties\n // We add these properties dynamically, so TypeScript needs to trust us here\n const routerCtx = ctx as TContext & RouterContext;\n if (!routerCtx.params) {\n routerCtx.params = {};\n }\n if (!routerCtx.router) {\n routerCtx.router = router;\n }\n if (!routerCtx._matchedRoute) {\n routerCtx._matchedRoute = path;\n }\n\n // Execute all middlewares in the stack (validation, then handler)\n // Build the middleware chain from right to left\n let index = -1;\n\n const dispatch = async (i: number): Promise<void> => {\n if (i <= index) {\n throw new Error('next() called multiple times');\n }\n index = i;\n\n if (i >= route.stack.length) {\n return;\n }\n\n const middleware = route.stack[i] as (ctx: unknown, next: () => Promise<void>) => Promise<void>;\n return await middleware(routerCtx, () => dispatch(i + 1));\n };\n\n await dispatch(0);\n}\n"],"names":["createMockContext","overrides","ctx","request","body","headers","response","status","undefined","params","query","state","method","path","get","field","toLowerCase","set","value","createMockUser","userUuid","email","firstName","lastName","avatar","role","isSubscriptionActive","createAuthenticatedContext","userOverrides","contextOverrides","user","authorization","mockDatabase","mockModel","methods","findOne","findAll","findByPk","create","update","destroy","count","flushPromises","Promise","resolve","setImmediate","assertions","assertDefined","message","Error","assertStatusCode","expected","JSON","stringify","assertResponseShape","expectedKeys","actualKeys","Object","keys","missingKeys","filter","key","includes","length","join","assertExists","assertType","type","validateObjectProperties","obj","expectedProps","every","prop","executeRoute","router","route","stack","find","layer","routerCtx","_matchedRoute","index","dispatch","i","middleware"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/testing/testHelpers.ts"],"sourcesContent":["import type { UserTypes } from '../models/UserInterfaces.js';\n\n// Mock type definition to avoid jest-mock dependency\nexport type Mock = any;\n\n\nexport interface BaseContext {\n request: {\n body?: unknown;\n headers?: Record<string, string | string[] | undefined>;\n files?: unknown;\n };\n response: {\n status: number;\n body?: unknown;\n };\n params?: Record<string, string>;\n query?: Record<string, string | string[] | undefined>;\n state: Record<string, unknown>;\n status: number;\n body?: unknown;\n headers: Record<string, string | string[] | undefined>;\n method?: string;\n path?: string;\n get: (field: string) => string | string[] | undefined;\n set: (field: string, value: string) => void;\n}\n\n\nexport function createMockContext<T extends BaseContext = BaseContext>(\n overrides: Partial<T> = {},\n): T {\n const ctx: BaseContext = {\n request: {\n body: {},\n headers: {},\n ...overrides.request,\n },\n response: {\n status: 200,\n body: undefined,\n ...overrides.response,\n },\n params: overrides.params || {},\n query: overrides.query || {},\n state: overrides.state || {},\n status: overrides.status || 200,\n body: overrides.body,\n headers: overrides.headers || {},\n method: overrides.method || 'GET',\n path: overrides.path || '/',\n get: (field: string) => {\n return ctx.headers[field.toLowerCase()];\n },\n set: (field: string, value: string) => {\n ctx.headers[field.toLowerCase()] = value;\n },\n };\n\n return { ...ctx, ...overrides } as T;\n}\n\n\nexport function createMockUser(overrides: Partial<UserTypes> = {}): UserTypes {\n return {\n userUuid: overrides.userUuid || 'test-user-uuid',\n email: overrides.email || 'test@example.com',\n firstName: overrides.firstName || 'Test',\n lastName: overrides.lastName || 'User',\n avatar: overrides.avatar || '1',\n role: overrides.role || 'user',\n isSubscriptionActive: overrides.isSubscriptionActive ?? true,\n ...overrides,\n };\n}\n\n\nexport interface AuthenticatedContext extends BaseContext {\n state: {\n user: UserTypes;\n [key: string]: unknown;\n };\n}\n\n\nexport function createAuthenticatedContext<T extends BaseContext = AuthenticatedContext>(\n userOverrides: Partial<UserTypes> = {},\n contextOverrides: Partial<T> = {},\n): T {\n const user = createMockUser(userOverrides);\n\n return createMockContext<T>({\n ...contextOverrides,\n state: {\n user,\n ...(contextOverrides.state || {}),\n },\n headers: {\n authorization: 'Bearer mock-token',\n ...(contextOverrides.headers || {}),\n },\n } as Partial<T>);\n}\n\nexport interface SequelizeModelMethods<T = unknown> {\n findOne: ((options?: unknown) => Promise<T | null>) | undefined;\n findAll: ((options?: unknown) => Promise<T[]>) | undefined;\n findByPk: ((id: string | number, options?: unknown) => Promise<T | null>) | undefined;\n create: ((values: Partial<T>, options?: unknown) => Promise<T>) | undefined;\n update: ((values: Partial<T>, options?: unknown) => Promise<[number, T[]]>) | undefined;\n destroy: ((options?: unknown) => Promise<number>) | undefined;\n count: ((options?: unknown) => Promise<number>) | undefined;\n}\n\n\nexport const mockDatabase = {\n\n mockModel<T = unknown>(\n methods: Partial<SequelizeModelMethods<T>> = {},\n ): SequelizeModelMethods<T> {\n return {\n findOne: undefined,\n findAll: undefined,\n findByPk: undefined,\n create: undefined,\n update: undefined,\n destroy: undefined,\n count: undefined,\n ...methods,\n };\n },\n};\n\n\nexport async function flushPromises(): Promise<void> {\n return new Promise((resolve) => setImmediate(resolve));\n}\n\n/**\n * ============================================================================\n * TEST DATA FACTORIES\n * Following industry best practices from Testing Blueprint\n * ============================================================================\n */\n\n/**\n * Constants for testing - avoids magic values\n */\nexport const TEST_CONSTANTS = {\n VALID_UUID: '123e4567-e89b-12d3-a456-426614174000',\n INVALID_UUID: 'invalid-uuid',\n VALID_TIMEZONE: 'America/New_York',\n INVALID_TIMEZONE: 'Invalid/Timezone',\n DEFAULT_TIMEZONE: 'UTC',\n TEST_DATE: '2026-02-25T10:00:00Z',\n WEEK_START: '2026-02-23',\n MAX_JSON_SIZE: 1024 * 1024, // 1MB\n} as const;\n\n/**\n * Mock file upload structure (multer format)\n */\nexport interface MockFile {\n fieldname: string;\n originalname: string;\n encoding: string;\n mimetype: string;\n buffer: Buffer;\n size: number;\n}\n\n/**\n * Creates a mock file for upload testing\n */\nexport function createMockFile(overrides: Partial<MockFile> = {}): MockFile {\n return {\n fieldname: 'photos',\n originalname: 'test-photo.jpg',\n encoding: '7bit',\n mimetype: 'image/jpeg',\n buffer: Buffer.from('fake-image-data'),\n size: 1024,\n ...overrides,\n };\n}\n\n/**\n * Creates multiple mock files\n */\nexport function createMockFiles(count: number): MockFile[] {\n return Array.from({ length: count }, (_, i) =>\n createMockFile({\n originalname: `test-photo-${i}.jpg`,\n size: 1024 * (i + 1),\n }),\n );\n}\n\n/**\n * Mock transaction for database testing\n */\nexport interface MockTransaction {\n commit: Mock;\n rollback: Mock;\n finished?: string;\n}\n\n/**\n * Creates a mock Sequelize transaction\n */\nexport function createMockTransaction(): MockTransaction {\n const mockFn = (globalThis as any).jest?.fn || (() => {\n throw new Error('jest.fn is not available. Make sure jest is properly configured.');\n });\n\n return {\n commit: mockFn().mockResolvedValue(undefined),\n rollback: mockFn().mockResolvedValue(undefined),\n };\n}\n\n/**\n * ============================================================================\n * MOCK CONFIGURATIONS\n * Pre-configured mocks for common testing scenarios\n * ============================================================================\n */\n\n/**\n * Database mock configuration\n */\nexport interface DatabaseMocks {\n findOne: Mock;\n findOrCreate: Mock;\n findByPk: Mock;\n findAll: Mock;\n create: Mock;\n update: Mock;\n upsert: Mock;\n bulkCreate: Mock;\n destroy: Mock;\n reset: () => void;\n restore: () => void;\n}\n\n/**\n * Creates a set of database operation mocks with reset/restore functionality\n */\nexport function setupDatabaseMocks(): DatabaseMocks {\n // Note: jest must be imported in the test file for this to work\n const mockFn = (globalThis as any).jest?.fn || (() => {\n throw new Error('jest.fn is not available. Make sure jest is properly configured.');\n });\n\n const mocks = {\n findOne: mockFn(),\n findOrCreate: mockFn(),\n findByPk: mockFn(),\n findAll: mockFn(),\n create: mockFn(),\n update: mockFn(),\n upsert: mockFn(),\n bulkCreate: mockFn(),\n destroy: mockFn(),\n };\n\n return {\n ...mocks,\n reset: () => {\n Object.values(mocks).forEach((mock) => mock.mockReset?.());\n },\n restore: () => {\n Object.values(mocks).forEach((mock) => mock.mockRestore?.());\n },\n };\n}\n\n/**\n * Time-related mock utilities\n */\nexport interface TimeMocks {\n setTime: (date: string | Date) => void;\n advanceTime: (ms: number) => void;\n restore: () => void;\n}\n\n/**\n * Sets up fake timers for timezone testing\n * Usage:\n * ```typescript\n * describe('My test', () => {\n * const timeMocks = setupTimeMocks();\n *\n * it('test with fixed time', () => {\n * timeMocks.setTime('2026-02-25T10:00:00Z');\n * // ... test code\n * });\n * });\n * ```\n */\nexport function setupTimeMocks(): TimeMocks {\n const jestGlobal = (globalThis as any).jest;\n const beforeEachGlobal = (globalThis as any).beforeEach;\n const afterEachGlobal = (globalThis as any).afterEach;\n\n if (beforeEachGlobal) {\n beforeEachGlobal(() => {\n jestGlobal?.useFakeTimers();\n });\n }\n\n if (afterEachGlobal) {\n afterEachGlobal(() => {\n jestGlobal?.useRealTimers();\n });\n }\n\n return {\n setTime: (date: string | Date) => {\n jestGlobal?.setSystemTime(new Date(date));\n },\n advanceTime: (ms: number) => {\n jestGlobal?.advanceTimersByTime(ms);\n },\n restore: () => {\n jestGlobal?.useRealTimers();\n },\n };\n}\n\n/**\n * Mock error instances for testing error handling\n */\nexport class MockValidationError extends Error {\n constructor(\n message: string,\n public issues: any[] = [],\n ) {\n super(message);\n this.name = 'ValidationError';\n }\n}\n\nexport class MockNotFoundError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NotFoundError';\n }\n}\n\nexport class MockDatabaseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SequelizeDatabaseError';\n }\n}\n\n/**\n * Helper to create a typed mock function\n */\nexport function createTypedMock(): Mock {\n const mockFn = (globalThis as any).jest?.fn || (() => {\n throw new Error('jest.fn is not available. Make sure jest is properly configured.');\n });\n return mockFn() as Mock;\n}\n\n/**\n * Helper to create a typed spy on a prototype method\n * Usage:\n * ```typescript\n * const mockMethod = spyOnPrototype(MyService.prototype, 'methodName');\n * mockMethod.mockResolvedValue({ success: true });\n * ```\n */\nexport function spyOnPrototype<T, K extends keyof T>(\n prototype: T,\n method: K,\n): Mock {\n const jestGlobal = (globalThis as any).jest;\n if (!jestGlobal?.spyOn) {\n throw new Error('jest.spyOn is not available. Make sure jest is properly configured.');\n }\n return jestGlobal.spyOn(prototype as any, method as any) as any;\n}\n\n\nexport const assertions = {\n\n assertDefined<T>(value: T | null | undefined, message?: string): asserts value is T {\n if (value === null || value === undefined) {\n throw new Error(message || 'Expected value to be defined');\n }\n },\n\n\n assertStatusCode(ctx: BaseContext | { status: number; body?: unknown }, expected: number): void {\n if (ctx.status !== expected) {\n throw new Error(\n `Expected status ${expected} but got ${ctx.status}. Body: ${JSON.stringify(ctx.body)}`,\n );\n }\n },\n\n assertResponseShape(\n body: unknown,\n expectedKeys: string[],\n ): asserts body is Record<string, unknown> {\n if (typeof body !== 'object' || body === null) {\n throw new Error('Expected body to be an object');\n }\n const actualKeys = Object.keys(body);\n const missingKeys = expectedKeys.filter((key) => !actualKeys.includes(key));\n if (missingKeys.length > 0) {\n throw new Error(\n `Missing expected keys in response: ${missingKeys.join(', ')}. Got: ${actualKeys.join(', ')}`,\n );\n }\n },\n\n\n assertExists<T>(value: T | null | undefined, message?: string): asserts value is T {\n if (value === null || value === undefined) {\n throw new Error(message || 'Expected value to exist');\n }\n },\n\n assertType<T>(value: unknown, type: string, message?: string): asserts value is T {\n if (typeof value !== type) {\n throw new Error(message || `Expected value to be of type ${type}, got ${typeof value}`);\n }\n },\n\n validateObjectProperties<T extends Record<string, unknown>>(\n obj: unknown,\n expectedProps: (keyof T)[],\n ): obj is T {\n if (typeof obj !== 'object' || obj === null) {\n return false;\n }\n return expectedProps.every((prop) => prop in obj);\n },\n};\n\nexport interface RouterContext extends BaseContext {\n params: Record<string, string>;\n router?: unknown;\n _matchedRoute?: string;\n _matchedRouteName?: string;\n}\n\n\nexport type RouterMiddleware<TContext = BaseContext> = (\n ctx: TContext,\n next: () => Promise<void>\n) => Promise<void> | void;\n\n\nexport interface RouterLayer<TContext = BaseContext> {\n path: string | RegExp;\n methods: string[];\n stack: RouterMiddleware<TContext>[];\n name?: string | null;\n}\n\n\nexport interface RouterLike<TContext = BaseContext> {\n stack: RouterLayer<TContext>[];\n}\n\nexport async function executeRoute<TContext extends BaseContext = BaseContext>(\n router: { stack: { path: string | RegExp; methods: string[]; stack: unknown[] }[] },\n path: string,\n method: string,\n ctx: TContext,\n): Promise<void> {\n const route = router.stack.find(\n (layer) => layer.path === path && layer.methods.includes(method)\n );\n\n if (!route) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Cast context to include router properties\n // We add these properties dynamically, so TypeScript needs to trust us here\n const routerCtx = ctx as TContext & RouterContext;\n if (!routerCtx.params) {\n routerCtx.params = {};\n }\n if (!routerCtx.router) {\n routerCtx.router = router;\n }\n if (!routerCtx._matchedRoute) {\n routerCtx._matchedRoute = path;\n }\n\n // Execute all middlewares in the stack (validation, then handler)\n // Build the middleware chain from right to left\n let index = -1;\n\n const dispatch = async (i: number): Promise<void> => {\n if (i <= index) {\n throw new Error('next() called multiple times');\n }\n index = i;\n\n if (i >= route.stack.length) {\n return;\n }\n\n const middleware = route.stack[i] as (ctx: unknown, next: () => Promise<void>) => Promise<void>;\n return await middleware(routerCtx, () => dispatch(i + 1));\n };\n\n await dispatch(0);\n}\n"],"names":["createMockContext","overrides","ctx","request","body","headers","response","status","undefined","params","query","state","method","path","get","field","toLowerCase","set","value","createMockUser","userUuid","email","firstName","lastName","avatar","role","isSubscriptionActive","createAuthenticatedContext","userOverrides","contextOverrides","user","authorization","mockDatabase","mockModel","methods","findOne","findAll","findByPk","create","update","destroy","count","flushPromises","Promise","resolve","setImmediate","TEST_CONSTANTS","VALID_UUID","INVALID_UUID","VALID_TIMEZONE","INVALID_TIMEZONE","DEFAULT_TIMEZONE","TEST_DATE","WEEK_START","MAX_JSON_SIZE","createMockFile","fieldname","originalname","encoding","mimetype","buffer","Buffer","from","size","createMockFiles","Array","length","_","i","createMockTransaction","mockFn","globalThis","jest","fn","Error","commit","mockResolvedValue","rollback","setupDatabaseMocks","mocks","findOrCreate","upsert","bulkCreate","reset","Object","values","forEach","mock","mockReset","restore","mockRestore","setupTimeMocks","jestGlobal","beforeEachGlobal","beforeEach","afterEachGlobal","afterEach","useFakeTimers","useRealTimers","setTime","date","setSystemTime","Date","advanceTime","ms","advanceTimersByTime","MockValidationError","message","issues","name","MockNotFoundError","MockDatabaseError","createTypedMock","spyOnPrototype","prototype","spyOn","assertions","assertDefined","assertStatusCode","expected","JSON","stringify","assertResponseShape","expectedKeys","actualKeys","keys","missingKeys","filter","key","includes","join","assertExists","assertType","type","validateObjectProperties","obj","expectedProps","every","prop","executeRoute","router","route","stack","find","layer","routerCtx","_matchedRoute","index","dispatch","middleware"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "90dc-core",
3
- "version": "1.16.13",
3
+ "version": "1.16.14",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",