diff --git a/package.json b/package.json index 60eb2e2d..893b3662 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,15 @@ }, "dependencies": { "@mailchimp/mailchimp_marketing": "^3.0.80", + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.3.6", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.3.6", "@nestjs/platform-express": "^10.3.7", "@nestjs/swagger": "^7.3.1", + "@nestjs/terminus": "^10.2.3", "@nestjs/typeorm": "^10.0.2", - "axios": "^1.6.8", + "axios": "^1.7.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "date-fns": "^3.6.0", diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index 86d4b648..00000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Bloom Backend!"', () => { - expect(appController.getHello()).toBe('Bloom Backend!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index bc499bfd..00000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller() -export class AppController { - @Get('/') - getHello(): string { - return 'Bloom Backend!'; - } -} diff --git a/src/app.module.ts b/src/app.module.ts index bceadd8e..9929f054 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { CourseUserModule } from './course-user/course-user.module'; import { CourseModule } from './course/course.module'; import { EventLoggerModule } from './event-logger/event-logger.module'; import { FeatureModule } from './feature/feature.module'; +import { HealthModule } from './health/health.module'; import { LoggerModule } from './logger/logger.module'; import { PartnerAccessModule } from './partner-access/partner-access.module'; import { PartnerAdminModule } from './partner-admin/partner-admin.module'; @@ -39,6 +40,7 @@ import { WebhooksModule } from './webhooks/webhooks.module'; FeatureModule, PartnerFeatureModule, EventLoggerModule, + HealthModule, ], }) export class AppModule {} diff --git a/src/health/health.controller.spec.ts b/src/health/health.controller.spec.ts new file mode 100644 index 00000000..d8988c31 --- /dev/null +++ b/src/health/health.controller.spec.ts @@ -0,0 +1,21 @@ +import { HttpModule } from '@nestjs/axios'; +import { TerminusModule } from '@nestjs/terminus'; +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller'; + +describe('HealthController', () => { + let controller: HealthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [TerminusModule, HttpModule], + controllers: [HealthController], + }).compile(); + + controller = module.get(HealthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts new file mode 100644 index 00000000..01f49a9d --- /dev/null +++ b/src/health/health.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheck, + HealthCheckService, + HttpHealthIndicator, + TypeOrmHealthIndicator, +} from '@nestjs/terminus'; +import { frontendAppUrl } from 'src/utils/constants'; + +@Controller('ping') +export class HealthController { + constructor( + private readonly health: HealthCheckService, + private readonly http: HttpHealthIndicator, + private readonly db: TypeOrmHealthIndicator, + ) {} + + @Get() + @HealthCheck() + ping() { + return 'ok'; + } + + @Get('/frontend') + @HealthCheck() + checkFrontend() { + return this.health.check([() => this.http.pingCheck('frontend', frontendAppUrl)]); + } + + @Get('/database') + @HealthCheck() + checkDatabase() { + return this.health.check([() => this.db.pingCheck('database')]); + } +} diff --git a/src/health/health.module.ts b/src/health/health.module.ts new file mode 100644 index 00000000..328a8d59 --- /dev/null +++ b/src/health/health.module.ts @@ -0,0 +1,10 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; +import { HealthController } from './health.controller'; + +@Module({ + imports: [TerminusModule, HttpModule], + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/src/main.ts b/src/main.ts index 98f7233f..3b8e995a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,9 @@ async function bootstrap() { app.setGlobalPrefix('api'); + // Starts listening for shutdown hooks + app.enableShutdownHooks(); + const options = new DocumentBuilder() .setTitle('Bloom backend API') .setDescription('Bloom backend API') diff --git a/src/partner-access/partner-access.service.spec.ts b/src/partner-access/partner-access.service.spec.ts index ff093ef6..bfd5dd88 100644 --- a/src/partner-access/partner-access.service.spec.ts +++ b/src/partner-access/partner-access.service.spec.ts @@ -60,6 +60,8 @@ describe('PartnerAccessService', () => { let mockPartnerAccessRepository: DeepMocked>; beforeEach(async () => { + jest.clearAllMocks(); + mockPartnerRepository = createMock>(mockPartnerRepositoryMethods); mockPartnerAccessRepository = createMock>( mockPartnerAccessRepositoryMethods, @@ -136,7 +138,7 @@ describe('PartnerAccessService', () => { return { ...mockPartnerAccessEntity, id: 'pa1', - userId: mockGetUserDto.user.id, + userId: mockUserEntity.id, }; }); // Mocks that the accesscode already exists @@ -146,13 +148,12 @@ describe('PartnerAccessService', () => { expect(partnerAccess).toEqual({ ...mockPartnerAccessEntity, - id: 'pa1', userId: mockUserEntity.id, activatedAt: partnerAccess.activatedAt, }); expect(profileData.updateServiceUserProfilesPartnerAccess).toHaveBeenCalledWith( - [partnerAccess], + [mockPartnerAccessEntity], mockUserEntity.email, ); }); diff --git a/src/partner-access/partner-access.service.ts b/src/partner-access/partner-access.service.ts index fc725cda..dda828e1 100644 --- a/src/partner-access/partner-access.service.ts +++ b/src/partner-access/partner-access.service.ts @@ -187,9 +187,12 @@ export class PartnerAccessService { assignedPartnerAccess.partner = partnerAccess.partner; try { - const partnerAccesses = await this.partnerAccessRepository.findBy({ - userId: user.id, - active: true, + const partnerAccesses = await this.partnerAccessRepository.find({ + where: { + userId: user.id, + active: true, + }, + relations: { partner: true }, }); updateServiceUserProfilesPartnerAccess(partnerAccesses, user.email); } catch (error) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e0500ba4..a6839960 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -53,17 +53,26 @@ export enum COMMUNICATION_SERVICE { MAILCHIMP = 'MAILCHIMP', } +export enum ENVIRONMENTS { + DEVELOPMENT = 'development', + STAGING = 'staging', + PRODUCTION = 'production', + TEST = 'test', +} + const getEnv = (env: string, envName: string): string => { try { - if (!env) throw `Unable to get environemt variable ${envName}`; + if (!env) throw `Unable to get environment variable ${envName}`; return env; } catch (error) { - console.log(error); + if (nodeEnv !== ENVIRONMENTS.TEST) console.log(error); } }; -export const isProduction = getEnv(process.env.NODE_ENV, 'NODE_ENV') === 'production'; +export const nodeEnv = getEnv(process.env.NODE_ENV, 'NODE_ENV'); +export const isProduction = nodeEnv === ENVIRONMENTS.PRODUCTION; +export const frontendAppUrl = getEnv(process.env.FRONTEND_APP_URL, 'FRONTEND_APP_URL'); export const rollbarEnv = getEnv(process.env.ROLLBAR_ENV, 'ROLLBAR_ENV'); export const rollbarToken = getEnv(process.env.ROLLBAR_TOKEN, 'ROLLBAR_TOKEN'); diff --git a/src/utils/serviceUserProfiles.ts b/src/utils/serviceUserProfiles.ts index f8c27999..93de52ea 100644 --- a/src/utils/serviceUserProfiles.ts +++ b/src/utils/serviceUserProfiles.ts @@ -208,6 +208,11 @@ export const serializePartnersString = (partnerAccesses: PartnerAccessEntity[]) return partnerAccesses?.map((pa) => pa.partner.name.toLowerCase()).join('; ') || ''; }; +const serializeCrispPartnerSegments = (partners: PartnerEntity[]) => { + if (!partners.length) return ['public']; + return partners.map((p) => p.name.toLowerCase()); +}; + const serializeUserData = (user: UserEntity) => { const { name, signUpLanguage, contactPermission, serviceEmailsPermission } = user; @@ -356,8 +361,3 @@ const serializeCourseData = (courseUser: CourseUserEntity) => { return { crispSchema, mailchimpSchema }; }; - -const serializeCrispPartnerSegments = (partners: PartnerEntity[]) => { - if (!partners.length) return ['public']; - return partners.map((p) => p.name.toLowerCase()); -}; diff --git a/test/utils/mockedServices.ts b/test/utils/mockedServices.ts index 2f3c680b..79fb4242 100644 --- a/test/utils/mockedServices.ts +++ b/test/utils/mockedServices.ts @@ -35,7 +35,6 @@ import { mockTherapySessionEntity, mockUserEntity, mockUserRecord, - partnerAccessArray, } from './mockData'; import { createQueryBuilderMock } from './mockUtils'; @@ -209,11 +208,8 @@ export const mockPartnerAccessRepositoryMethods: PartialFuncReturn< findBy: async (arg) => { return [{ ...mockPartnerAccessEntity, ...(arg ? { ...arg } : {}) }] as PartnerAccessEntity[]; }, - find: async (arg) => { - return [ - ...partnerAccessArray, - { ...mockPartnerAccessEntity, ...(arg ? { ...arg } : {}) }, - ] as PartnerAccessEntity[]; + find: async () => { + return [{ ...mockPartnerAccessEntity }] as PartnerAccessEntity[]; }, save: async (arg) => arg as PartnerAccessEntity, }; diff --git a/yarn.lock b/yarn.lock index 54f0d1a2..a9c835c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1181,6 +1181,11 @@ resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== +"@nestjs/axios@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.0.2.tgz#0078c101a29fb46f5c566d68a4315fddabc083ed" + integrity sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ== + "@nestjs/cli@^10.3.2": version "10.3.2" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.3.2.tgz#42d2764ead6633e278c55d42de871b4cc1db002b" @@ -1279,6 +1284,14 @@ path-to-regexp "3.2.0" swagger-ui-dist "5.11.2" +"@nestjs/terminus@^10.2.3": + version "10.2.3" + resolved "https://registry.yarnpkg.com/@nestjs/terminus/-/terminus-10.2.3.tgz#72c8a66d04df52aeaae807551245480fd7239a75" + integrity sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg== + dependencies: + boxen "5.1.2" + check-disk-space "3.4.0" + "@nestjs/testing@^10.3.6": version "10.3.8" resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.3.8.tgz#44df73ede43c47801400d59a8ebd6ab1fe7df34c" @@ -2125,6 +2138,13 @@ ajv@^8.0.0: require-from-string "^2.0.2" uri-js "^4.4.1" +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-colors@4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -2262,6 +2282,15 @@ axios@^1.6.8: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -2369,6 +2398,20 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" +boxen@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2539,7 +2582,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -check-disk-space@^3.4.0: +check-disk-space@3.4.0, check-disk-space@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.4.0.tgz#eb8e69eee7a378fd12e35281b8123a8b4c4a8ff7" integrity sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw== @@ -2603,6 +2646,11 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -6565,7 +6613,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7011,6 +7059,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -7293,6 +7346,13 @@ which@^4.0.0: dependencies: isexe "^3.1.1" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + winston-transport@^4.5.0: version "4.7.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.7.0.tgz#e302e6889e6ccb7f383b926df6936a5b781bd1f0"