├── scripts ├── test.bash └── e2e.test.bash ├── upload └── single │ └── __keep__ ├── src ├── migration │ ├── data │ │ ├── .gitkeep │ │ ├── getAdapterQuery.json │ │ └── getTransformerQuery.json │ ├── utils │ │ ├── deleteTables.ts │ │ ├── hausraTrackAll.ts │ │ ├── runPrismaMigrate.ts │ │ └── downloadData.ts │ ├── .DS_Store │ ├── readme.md │ ├── migration.module.ts │ └── migration.service.ts ├── modules │ ├── bot │ │ ├── entities │ │ │ └── bot.entity.ts │ │ ├── dto │ │ │ ├── delete-bot-dto.ts │ │ │ ├── update-bot.dto.ts │ │ │ └── create-bot.dto.ts │ │ └── bot.module.ts │ ├── user-segment │ │ ├── entities │ │ │ └── user-segment.entity.ts │ │ ├── dto │ │ │ ├── create-user-segment.dto.ts │ │ │ └── update-user-segment.dto.ts │ │ ├── user-segment.module.ts │ │ ├── fusionauth │ │ │ ├── fusionauthClientProvider.ts │ │ │ ├── dummyData │ │ │ │ ├── generater.ts │ │ │ │ └── ingester.ts │ │ │ └── queryBuilder.ts │ │ ├── user-segment.controller.ts │ │ ├── user-segment.service.ts │ │ ├── user-segment.service.spec.ts │ │ └── user-segment.controller.spec.ts │ ├── form │ │ ├── formUpload.dto.ts │ │ ├── form.module.ts │ │ ├── form.controller.spec.ts │ │ ├── form.types.ts │ │ └── form.controller.ts │ ├── transformer │ │ ├── entities │ │ │ └── transformer.entity.ts │ │ ├── dto │ │ │ ├── update-transformer.dto.ts │ │ │ └── create-transformer.dto.ts │ │ ├── transformer.module.ts │ │ ├── transformer.service.spec.ts │ │ ├── transformer.service.ts │ │ ├── transformer.controller.ts │ │ └── transformer.controller.spec.ts │ ├── .DS_Store │ ├── conversation-logic │ │ ├── entities │ │ │ └── conversation-logic.entity.ts │ │ ├── dto │ │ │ ├── update-conversation-logic.dto.ts │ │ │ └── create-conversation-logic.dto.ts │ │ ├── conversation-logic.module.ts │ │ ├── conversation-logic.service.spec.ts │ │ ├── conversation-logic.controller.ts │ │ ├── conversation-logic.service.ts │ │ └── conversation-logic.controller.spec.ts │ ├── secrets │ │ ├── types │ │ │ ├── headers.secret.ts │ │ │ ├── hasura.secret.ts │ │ │ ├── whatsapp.netcore.secrets.ts │ │ │ ├── whatsapp.gupshup.secret.ts │ │ │ ├── index.spec.ts │ │ │ └── index.ts │ │ ├── secret.dto.ts │ │ ├── readme.md │ │ ├── secrets.module.ts │ │ ├── secrets.service.provider.ts │ │ ├── secrets.service.ts │ │ ├── secrets.controller.ts │ │ └── secrets.service.spec.ts │ ├── adapter │ │ ├── enums │ │ │ ├── channels.ts │ │ │ └── providers.ts │ │ ├── dto │ │ │ ├── index.ts │ │ │ ├── whatsapp.gupshup.dto.ts │ │ │ └── whatsapp.netcore.dto.ts │ │ ├── adapter.module.ts │ │ ├── adapter.service.ts │ │ ├── adapter.controller.ts │ │ ├── adapter.service.spec.ts │ │ └── adapter.controller.spec.ts │ ├── service │ │ ├── service.controller.ts │ │ ├── enum.ts │ │ ├── service-resolver.ts │ │ ├── service.controller.spec.ts │ │ ├── schema │ │ │ └── user.dto.ts │ │ ├── service.module.ts │ │ ├── types.ts │ │ ├── service.service.ts │ │ └── gql.resolver.spec.ts │ └── readme.md ├── interceptors │ ├── .readme.md │ ├── addAdminHeader.interceptor.ts │ ├── addOwnerInfo.interceptor.spec.ts │ ├── addROToResponse.interceptor.ts │ ├── multipleFiles.interceptor.ts │ ├── multipleFields.interceptor.ts │ ├── addResponseObject.interceptor.ts │ ├── file.interceptor.ts │ ├── utils │ │ └── responseUtils.ts │ ├── addAdminHeader.interceptor.spec.ts │ └── addOwnerInfo.interceptor.ts ├── .DS_Store ├── auth │ ├── user.entity.ts │ ├── public.decorator.ts │ ├── auth.guard.spec.ts │ ├── auth.service.ts │ ├── samagra.jwt.example.json │ ├── auth.service.spec.ts │ ├── auth.strategy.ts │ ├── sunbird.jwt.example.json │ ├── auth.module.ts │ ├── auth.helper.ts │ └── auth.guard.ts ├── common │ ├── prismaError.ts │ ├── prismaUtils.ts │ ├── retry.spec.ts │ ├── retry.ts │ ├── file-mapper.ts │ └── prismaError.handler.ts ├── sunbird-telemetry │ ├── sunbird-telemetry.module.ts │ ├── sunbird-telemetry.controller.spec.ts │ └── sunbird-telemetry.controller.ts ├── monitoring │ └── monitoring.module.ts ├── pipes │ └── validation-pipe-options.ts ├── app.service.ts ├── app.controller.ts ├── global-services │ ├── commonService.module.ts │ ├── telemetry.service.ts │ └── prisma.service.ts ├── console.ts ├── health │ ├── health.module.ts │ ├── health.controller.ts │ ├── health.controller.spec.ts │ └── health.service.spec.ts ├── todo.md ├── app.controller.spec.ts ├── main.ts └── app.module.ts ├── .dockerignore ├── contribution └── debug.md ├── test ├── prisma.mock.ts ├── jest-e2e.json └── app.e2e-spec.ts ├── .prettierrc ├── docs ├── conn.png ├── techover.png └── funcblocks.png ├── nest-cli.json ├── prisma ├── migrations │ ├── 20240718084125_ │ │ └── migration.sql │ ├── 20220822130632_bot_add_tags │ │ └── migration.sql │ ├── 20220822130634_bot_add_image │ │ └── migration.sql │ ├── 20240403104825_add_meta_in_bot │ │ └── migration.sql │ ├── 20220327155715_transformer_servic_not_unique │ │ └── migration.sql │ ├── 20240226082219_add_pinned_status_to_bot_status │ │ └── migration.sql │ ├── 20220327113504_optional_description │ │ └── migration.sql │ ├── 20220327160847_usersegment_category_optional │ │ └── migration.sql │ ├── 20220327162041_transformer_config_remove_unique_constraint │ │ └── migration.sql │ ├── 20240802101405_add_name_in_schedule_table │ │ └── migration.sql │ ├── 20220327160543_usersegment_description_optional │ │ └── migration.sql │ ├── migration_lock.toml │ ├── 20220327112638_optional_start_date_end_date │ │ └── migration.sql │ ├── 20220327114451_bot_start_date_date_not_time │ │ └── migration.sql │ ├── 20240802044019_change_start_end_date_to_timestamp │ │ └── migration.sql │ ├── 20220327113749_optional_owner_id_owner_o_rg_id │ │ └── migration.sql │ ├── 20220327161010_usersegment_service_remove_unique_constraint │ │ └── migration.sql │ ├── 20220402120029_ │ │ └── migration.sql │ ├── 20220327074749_add_cl_name │ │ └── migration.sql │ ├── 20220601174909_add_status_to_bot │ │ └── migration.sql │ ├── 20220327113624_optional_description │ │ └── migration.sql │ ├── 20220327080800_udpate_transformer_config_in_cl_2 │ │ └── migration.sql │ ├── 20220404042658_ │ │ └── migration.sql │ ├── 20240718081636_add_schedule_table │ │ └── migration.sql │ ├── 20240801091425_add_foriegn_key_to_bot_in_schedule │ │ └── migration.sql │ ├── 20220327072038_fix_remove_owner_id │ │ └── migration.sql │ ├── 20220203070524_tst_migration │ │ └── migration.sql │ ├── 20220327160740_usersegment_services_optional │ │ └── migration.sql │ ├── 20220327080251_udpate_transformer_config_in_cl │ │ └── migration.sql │ └── 20220203070953_fix_mtom_relations │ │ └── migration.sql └── schema.prisma ├── tsconfig.build.json ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── c4gt.md └── workflows │ ├── CI-tests.yml │ ├── docker-push.yml │ └── codeql-analysis.yml ├── tsconfig.json ├── Dockerfile ├── .eslintrc.js ├── .vscode └── settings.json ├── docker-compose.local.yml └── package.json /scripts/test.bash: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/e2e.test.bash: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /upload/single/__keep__: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/migration/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/migration/utils/deleteTables.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/migration/utils/hausraTrackAll.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /contribution/debug.md: -------------------------------------------------------------------------------- 1 | `yarn start:debug` 2 | -------------------------------------------------------------------------------- /test/prisma.mock.ts: -------------------------------------------------------------------------------- 1 | export const prisma = {}; 2 | -------------------------------------------------------------------------------- /src/migration/utils/runPrismaMigrate.ts: -------------------------------------------------------------------------------- 1 | //ts-ignore 2 | -------------------------------------------------------------------------------- /src/modules/bot/entities/bot.entity.ts: -------------------------------------------------------------------------------- 1 | export class Bot {} 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/interceptors/.readme.md: -------------------------------------------------------------------------------- 1 | Inceptors to modifiy headers and verify requests. 2 | -------------------------------------------------------------------------------- /docs/conn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samagra-comms/uci-apis/HEAD/docs/conn.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samagra-comms/uci-apis/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/modules/user-segment/entities/user-segment.entity.ts: -------------------------------------------------------------------------------- 1 | export class UserSegment {} 2 | -------------------------------------------------------------------------------- /docs/techover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samagra-comms/uci-apis/HEAD/docs/techover.png -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/form/formUpload.dto.ts: -------------------------------------------------------------------------------- 1 | export class FormUploadDto { 2 | form: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/transformer/entities/transformer.entity.ts: -------------------------------------------------------------------------------- 1 | export class ConversationLogic {} 2 | -------------------------------------------------------------------------------- /docs/funcblocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samagra-comms/uci-apis/HEAD/docs/funcblocks.png -------------------------------------------------------------------------------- /src/modules/user-segment/dto/create-user-segment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateUserSegmentDto {} 2 | -------------------------------------------------------------------------------- /prisma/migrations/20240718084125_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Schedules_botId_key"; 3 | -------------------------------------------------------------------------------- /src/modules/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samagra-comms/uci-apis/HEAD/src/modules/.DS_Store -------------------------------------------------------------------------------- /src/migration/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samagra-comms/uci-apis/HEAD/src/migration/.DS_Store -------------------------------------------------------------------------------- /src/modules/conversation-logic/entities/conversation-logic.entity.ts: -------------------------------------------------------------------------------- 1 | export class ConversationLogic {} 2 | -------------------------------------------------------------------------------- /src/auth/user.entity.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | public ownerId: string; 3 | public ownerOrgId: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/secrets/types/headers.secret.ts: -------------------------------------------------------------------------------- 1 | type Secret = { [key: string]: string }; 2 | export default Secret; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220822130632_bot_add_tags/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ADD COLUMN "tags" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220822130634_bot_add_image/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ADD COLUMN "botImage" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240403104825_add_meta_in_bot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ADD COLUMN "meta" JSONB; 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /prisma/migrations/20220327155715_transformer_servic_not_unique/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Transformer_serviceId_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240226082219_add_pinned_status_to_bot_status/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "BotStatus" ADD VALUE 'PINNED'; 3 | -------------------------------------------------------------------------------- /src/modules/adapter/enums/channels.ts: -------------------------------------------------------------------------------- 1 | export enum Channel { 2 | SMS = 'SMS', 3 | EMAIL = 'Email', 4 | WHATSAPP = 'Whatsapp', 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/secrets/types/hasura.secret.ts: -------------------------------------------------------------------------------- 1 | type Secret = { 2 | baseURL: string; 3 | adminSecret: string; 4 | }; 5 | 6 | export default Secret; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20220327113504_optional_description/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "UserSegment" ALTER COLUMN "description" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /src/modules/secrets/types/whatsapp.netcore.secrets.ts: -------------------------------------------------------------------------------- 1 | type Secret = { 2 | source: string; 3 | bearer: string; 4 | }; 5 | 6 | export default Secret; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20220327160847_usersegment_category_optional/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "UserSegment" ALTER COLUMN "category" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220327162041_transformer_config_remove_unique_constraint/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "TransformerConfig_transformerId_key"; 3 | -------------------------------------------------------------------------------- /src/modules/bot/dto/delete-bot-dto.ts: -------------------------------------------------------------------------------- 1 | export class DeleteBotsDTO { 2 | ids: string[] | undefined | null; 3 | endDate: string | undefined | null; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/service/service.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | @Controller('service') 4 | export class ServiceController {} 5 | -------------------------------------------------------------------------------- /prisma/migrations/20240802101405_add_name_in_schedule_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Schedules" ADD COLUMN "name" TEXT NOT NULL DEFAULT E''; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220327160543_usersegment_description_optional/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "UserSegment" ALTER COLUMN "description" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /src/modules/adapter/enums/providers.ts: -------------------------------------------------------------------------------- 1 | export enum Provider { 2 | Gupshup = 'Gupshup', 3 | Netcore = 'Netcore', 4 | Facebook = 'Facebook', 5 | Twilio = 'Twilio', 6 | } 7 | -------------------------------------------------------------------------------- /src/auth/public.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const IS_PUBLIC_KEY = 'isPublic'; 4 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); -------------------------------------------------------------------------------- /src/common/prismaError.ts: -------------------------------------------------------------------------------- 1 | export type PrismaError = { 2 | code: string; 3 | message: string; 4 | stackTrace: string[] | null; 5 | field?: string; 6 | methodName?: string; 7 | }; 8 | -------------------------------------------------------------------------------- /prisma/migrations/20220327112638_optional_start_date_end_date/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ALTER COLUMN "startDate" DROP NOT NULL, 3 | ALTER COLUMN "endDate" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20220327114451_bot_start_date_date_not_time/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ALTER COLUMN "startDate" SET DATA TYPE DATE, 3 | ALTER COLUMN "endDate" SET DATA TYPE DATE; 4 | -------------------------------------------------------------------------------- /src/modules/secrets/types/whatsapp.gupshup.secret.ts: -------------------------------------------------------------------------------- 1 | type Secret = { 2 | usernameHSM: string; 3 | passwordHSM: string; 4 | username2Way: string; 5 | password2Way: string; 6 | }; 7 | export default Secret; 8 | -------------------------------------------------------------------------------- /src/modules/transformer/dto/update-transformer.dto.ts: -------------------------------------------------------------------------------- 1 | import { CreateConversationLogicDto } from './create-transformer.dto'; 2 | 3 | export interface UpdateConversationLogicDto extends CreateConversationLogicDto { } 4 | -------------------------------------------------------------------------------- /src/modules/secrets/secret.dto.ts: -------------------------------------------------------------------------------- 1 | import { Secret, SecretType } from './types'; 2 | 3 | export type SecretDTO = { 4 | secretBody: Secret; 5 | type: SecretType; 6 | variableName: string; 7 | ownerId: string; 8 | }; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240802044019_change_start_end_date_to_timestamp/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ALTER COLUMN "startDate" SET DATA TYPE TIMESTAMP(3), 3 | ALTER COLUMN "endDate" SET DATA TYPE TIMESTAMP(3); 4 | -------------------------------------------------------------------------------- /src/auth/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtAuthGuard } from './auth.guard'; 2 | 3 | describe.skip('AuthGuard', () => { 4 | it('should be defined', () => { 5 | // expect(new JwtAuthGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /prisma/migrations/20220327113749_optional_owner_id_owner_o_rg_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Bot" ALTER COLUMN "ownerID" DROP NOT NULL, 3 | ALTER COLUMN "ownerOrgID" DROP NOT NULL, 4 | ALTER COLUMN "purpose" DROP NOT NULL; 5 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | 4 | @Injectable() 5 | export class AuthService { 6 | constructor(private readonly jwtService: JwtService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/dto/update-conversation-logic.dto.ts: -------------------------------------------------------------------------------- 1 | import { CreateConversationLogicDto } from './create-conversation-logic.dto'; 2 | 3 | export interface UpdateConversationLogicDto extends CreateConversationLogicDto { } 4 | -------------------------------------------------------------------------------- /src/modules/service/enum.ts: -------------------------------------------------------------------------------- 1 | export enum ServiceType { 2 | GQL = 'gql', 3 | GET = 'GET', 4 | POST = 'POST', 5 | } 6 | 7 | export enum ServiceQueryType { 8 | byPhone = 'byPhone', 9 | byId = 'byId', 10 | all = 'all', 11 | } 12 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | dist 5 | documentation 6 | vault/* 7 | vault 8 | keys 9 | *.samples.json 10 | dist/* 11 | yarn-error.log 12 | 13 | prisma/generated/prisma-client-js -------------------------------------------------------------------------------- /src/migration/readme.md: -------------------------------------------------------------------------------- 1 | This module is use to migrate the data from the old version of the database to the new version. 2 | 3 | ## Scripts 4 | 5 | `yarn migrate delete-old-tables` 6 | `yarn migrate download-data` 7 | `yarn migrate import-data` 8 | -------------------------------------------------------------------------------- /src/common/prismaUtils.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '../../prisma/generated/prisma-client-js'; 2 | 3 | export const caseInsensitiveQueryBuilder = ( 4 | param: string, 5 | ): Prisma.StringFilter => ({ 6 | contains: param, 7 | mode: 'insensitive', 8 | }); 9 | -------------------------------------------------------------------------------- /src/modules/user-segment/dto/update-user-segment.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateUserSegmentDto } from './create-user-segment.dto'; 3 | 4 | export class UpdateUserSegmentDto extends PartialType(CreateUserSegmentDto) {} 5 | -------------------------------------------------------------------------------- /src/modules/secrets/readme.md: -------------------------------------------------------------------------------- 1 | Add a new secret to a userPath. The keys will be stored by ownerID. 2 | Secrets are stored based on spec. 3 | Examples: 4 | 5 | - user1/UserSegmentServer1 6 | - user1/UserSegmentServer1/key1 7 | 8 | - user1/secrets/test/key2 9 | POST 10 | -------------------------------------------------------------------------------- /src/modules/adapter/dto/index.ts: -------------------------------------------------------------------------------- 1 | import { WhatsappGupshupAdapterDTO } from './whatsapp.gupshup.dto'; 2 | import { WhatsappNetcoreAdapterDTO } from './whatsapp.netcore.dto'; 3 | 4 | type AdapterDTO = WhatsappGupshupAdapterDTO | WhatsappNetcoreAdapterDTO; 5 | 6 | export { AdapterDTO }; 7 | -------------------------------------------------------------------------------- /src/sunbird-telemetry/sunbird-telemetry.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SunbirdTelemetryController } from './sunbird-telemetry.controller'; 3 | 4 | @Module({ 5 | controllers: [SunbirdTelemetryController], 6 | }) 7 | export class SunbirdTelemetryModule {} 8 | -------------------------------------------------------------------------------- /prisma/migrations/20220327161010_usersegment_service_remove_unique_constraint/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "UserSegment_allServiceID_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "UserSegment_byIDServiceID_key"; 6 | 7 | -- DropIndex 8 | DROP INDEX "UserSegment_byPhoneServiceID_key"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20220402120029_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `name` on the `Adapter` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Adapter_name_key"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Adapter" DROP COLUMN "name"; 12 | -------------------------------------------------------------------------------- /src/modules/bot/dto/update-bot.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateBotDto } from './create-bot.dto'; 3 | 4 | export class UpdateBotDto extends PartialType(CreateBotDto) {} 5 | 6 | export type ModifyNotificationDTO = { 7 | title?: string, 8 | description?: string, 9 | } 10 | -------------------------------------------------------------------------------- /prisma/migrations/20220327074749_add_cl_name/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `name` to the `ConversationLogic` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ConversationLogic" ADD COLUMN "name" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /src/migration/migration.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { PrismaService } from '../global-services/prisma.service'; 4 | 5 | @Module({ 6 | imports: [ConfigModule], 7 | providers: [PrismaService], 8 | }) 9 | export class MigrationModule {} 10 | -------------------------------------------------------------------------------- /src/monitoring/monitoring.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MonitoringService } from './monitoring.service'; 3 | import { MonitoringController } from './monitoring.controller'; 4 | 5 | @Module({ 6 | providers: [ 7 | MonitoringService, 8 | ], 9 | controllers: [MonitoringController] 10 | }) 11 | export class MonitoringModule {} 12 | -------------------------------------------------------------------------------- /src/modules/adapter/dto/whatsapp.gupshup.dto.ts: -------------------------------------------------------------------------------- 1 | interface GupshupWhatsappAdapterConfig { 2 | phone: string; 3 | HSM_ID: string; 4 | '2WAY': string; 5 | credentials: { [key: string]: string }; 6 | } 7 | 8 | export type WhatsappGupshupAdapterDTO = { 9 | channel: string; 10 | provider: string; 11 | name: string; 12 | config: GupshupWhatsappAdapterConfig; 13 | }; 14 | -------------------------------------------------------------------------------- /src/modules/adapter/dto/whatsapp.netcore.dto.ts: -------------------------------------------------------------------------------- 1 | interface NetcoreWhatsappAdapterConfig { 2 | phone: string; 3 | HSM_ID: string; 4 | '2WAY': string; 5 | credentials: { [key: string]: string }; 6 | } 7 | 8 | export type WhatsappNetcoreAdapterDTO = { 9 | channel: string; 10 | provider: string; 11 | name: string; 12 | config: NetcoreWhatsappAdapterConfig; 13 | }; 14 | -------------------------------------------------------------------------------- /src/pipes/validation-pipe-options.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from '@nestjs/common'; 2 | import { ValidatorOptions } from '@nestjs/common/interfaces/external/validator-options.interface'; 3 | 4 | export interface ValidationPipeOptions extends ValidatorOptions { 5 | transform?: boolean; 6 | disableErrorMessages?: boolean; 7 | exceptionFactory?: (errors: ValidationError[]) => any; 8 | } 9 | -------------------------------------------------------------------------------- /prisma/migrations/20220601174909_add_status_to_bot/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "BotStatus" AS ENUM ('ENABLED', 'DISABLED', 'DRAFT'); 3 | 4 | -- DropIndex 5 | DROP INDEX "Adapter_name_key"; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Adapter" ALTER COLUMN "name" DROP NOT NULL; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Bot" ADD COLUMN "status" "BotStatus" NOT NULL DEFAULT E'DRAFT'; 12 | -------------------------------------------------------------------------------- /src/modules/service/service-resolver.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | } 4 | 5 | export interface ServiceResponse { 6 | data: T; 7 | } 8 | 9 | export abstract class ServiceResolver { 10 | private name: string; 11 | 12 | constructor(name: string) { 13 | this.name = name; 14 | } 15 | 16 | abstract resolve(string): Promise | null>; 17 | } 18 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from './global-services/prisma.service'; 3 | 4 | @Injectable() 5 | export class AppService { 6 | constructor(private prisma: PrismaService) {} 7 | getHello(): string { 8 | return 'Hello World!'; 9 | } 10 | 11 | getBotCount(): Promise { 12 | return this.prisma.bot.count(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /prisma/migrations/20220327113624_optional_description/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `description` on table `UserSegment` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Bot" ALTER COLUMN "description" DROP NOT NULL; 9 | 10 | -- AlterTable 11 | ALTER TABLE "UserSegment" ALTER COLUMN "description" SET NOT NULL; 12 | -------------------------------------------------------------------------------- /src/common/retry.spec.ts: -------------------------------------------------------------------------------- 1 | import { retryPromiseWithDelay } from "./retry"; 2 | 3 | describe('RetryMethod', () => { 4 | it('should retry an async await method', async () => { 5 | let callCounter = 0; 6 | const testFunction = async function createApplication(): Promise {} 7 | 8 | await retryPromiseWithDelay(testFunction, 2, 0); 9 | await expect(retryPromiseWithDelay(testFunction, 1, 0)).rejects; 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('/getBotCount') 9 | async getBotCount(): Promise { 10 | const botCount = await this.appService.getBotCount(); 11 | return { 12 | data: botCount, 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /prisma/migrations/20220327080800_udpate_transformer_config_in_cl_2/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `transformerId` on the `ConversationLogic` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "ConversationLogic" DROP CONSTRAINT "ConversationLogic_transformerId_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "ConversationLogic" DROP COLUMN "transformerId"; 12 | -------------------------------------------------------------------------------- /src/modules/transformer/transformer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TransformerService } from './transformer.service'; 3 | import { TransformerController } from './transformer.controller'; 4 | import { PrismaService } from '../../global-services/prisma.service'; 5 | 6 | @Module({ 7 | controllers: [TransformerController], 8 | providers: [TransformerService, PrismaService], 9 | }) 10 | export class TransformerModule { } 11 | -------------------------------------------------------------------------------- /src/modules/user-segment/user-segment.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserSegmentService } from './user-segment.service'; 3 | import { UserSegmentController } from './user-segment.controller'; 4 | import { PrismaService } from '../../global-services/prisma.service'; 5 | 6 | @Module({ 7 | controllers: [UserSegmentController], 8 | providers: [UserSegmentService, PrismaService], 9 | }) 10 | export class UserSegmentModule {} 11 | -------------------------------------------------------------------------------- /src/global-services/commonService.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TelemetryService } from './telemetry.service'; 3 | 4 | //Singleton pattern - https://stackoverflow.com/questions/60192912/how-to-create-a-service-that-acts-as-a-singleton-with-nestjs 5 | @Module({ 6 | providers: [TelemetryService], 7 | exports: [TelemetryService], 8 | }) 9 | export class CommonServiceModule { 10 | constructor(private telemetryService: TelemetryService) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/form/form.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FormController } from './form.controller'; 3 | import { FormService } from './form.service'; 4 | import { TelemetryService } from '../../global-services/telemetry.service'; 5 | import { PrismaService } from '../../global-services/prisma.service'; 6 | 7 | @Module({ 8 | controllers: [FormController], 9 | providers: [FormService, TelemetryService, PrismaService], 10 | }) 11 | export class FormModule {} 12 | -------------------------------------------------------------------------------- /src/auth/samagra.jwt.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "aud": "a1313380-069d-4f4f-8dcb-0d0e717f6a6b", 3 | "exp": 1650375374, 4 | "iat": 1650371774, 5 | "iss": "acme.com", 6 | "sub": "8f7ee860-0163-4229-9d2a-01cef53145ba", 7 | "jti": "d0ad322e-76fa-48b5-a879-27eef2ee7ac7", 8 | "authenticationType": "PASSWORD", 9 | "preferred_username": "uci-user", 10 | "applicationId": "a1313380-069d-4f4f-8dcb-0d0e717f6a6b", 11 | "roles": ["orgOwner"], 12 | "ownerOrgId": "uci-user", 13 | "ownerId": "uci-user" 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/conversation-logic.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConversationLogicService } from './conversation-logic.service'; 3 | import { ConversationLogicController } from './conversation-logic.controller'; 4 | import { PrismaService } from '../../global-services/prisma.service'; 5 | 6 | @Module({ 7 | controllers: [ConversationLogicController], 8 | providers: [ConversationLogicService, PrismaService], 9 | }) 10 | export class ConversationLogicModule {} 11 | -------------------------------------------------------------------------------- /src/modules/readme.md: -------------------------------------------------------------------------------- 1 | ## Core Modules 2 | 3 | Includes the modules related to all the entities in the system. 4 | 5 | 1. Adapter 6 | 2. Bot 7 | 3. Conversation Logic 8 | 4. Secrets 9 | 5. Service (Defines an external service - GQL, GET, POST, etc.) 10 | 6. Transformers 11 | 7. User Segments 12 | 13 | The structure of each of the modules follows the standard NestJS structure. 14 | 15 | - Service - Fetch data from DB using prisma or call an external API 16 | - Controllers - Manage routes 17 | - Spec.ts - Tests 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /prisma/migrations/20220404042658_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `Adapter` will be added. If there are existing duplicate values, this will fail. 5 | - Added the required column `name` to the `Adapter` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Adapter" ADD COLUMN "name" TEXT NOT NULL; 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "Adapter_name_key" ON "Adapter"("name"); 13 | -------------------------------------------------------------------------------- /prisma/migrations/20240718081636_add_schedule_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Schedules" ( 3 | "id" UUID NOT NULL DEFAULT gen_random_uuid(), 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "scheduledAt" TIMESTAMP(3) NOT NULL, 6 | "authToken" TEXT NOT NULL, 7 | "botId" TEXT NOT NULL, 8 | "config" JSONB NOT NULL, 9 | 10 | CONSTRAINT "Schedules_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "Schedules_botId_key" ON "Schedules"("botId"); 15 | -------------------------------------------------------------------------------- /src/modules/adapter/adapter.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdaptersService } from './adapter.service'; 3 | import { AdaptersController } from './adapter.controller'; 4 | import { PrismaService } from '../../global-services/prisma.service'; 5 | import { CommonServiceModule } from '../../global-services/commonService.module'; 6 | 7 | @Module({ 8 | controllers: [AdaptersController], 9 | imports: [CommonServiceModule], 10 | providers: [AdaptersService, PrismaService], 11 | }) 12 | export class AdaptersModule {} 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "esModuleInterop": true, 17 | "strictNullChecks": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe.skip('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /src/modules/secrets/secrets.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SecretsService } from './secrets.service'; 3 | import { SecretsController } from './secrets.controller'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { PrismaService } from '../../global-services/prisma.service'; 6 | import { VaultClientProvider } from '../secrets/secrets.service.provider'; 7 | 8 | @Module({ 9 | providers: [SecretsService, ConfigService, PrismaService,VaultClientProvider], 10 | controllers: [SecretsController], 11 | }) 12 | export class SecretsModule {} 13 | -------------------------------------------------------------------------------- /prisma/migrations/20240801091425_add_foriegn_key_to_bot_in_schedule/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Changed the type of `botId` on the `Schedules` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Schedules" DROP COLUMN "botId", 9 | ADD COLUMN "botId" UUID NOT NULL; 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "Schedules" ADD CONSTRAINT "Schedules_botId_fkey" FOREIGN KEY ("botId") REFERENCES "Bot"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | -------------------------------------------------------------------------------- /src/modules/form/form.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { FormController } from './form.controller'; 3 | 4 | describe.skip('FormController', () => { 5 | let controller: FormController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [FormController], 10 | }).compile(); 11 | 12 | controller = module.get(FormController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/modules/service/service.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ServiceController } from './service.controller'; 3 | 4 | describe('ServiceController', () => { 5 | let controller: ServiceController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ServiceController], 10 | }).compile(); 11 | 12 | controller = module.get(ServiceController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/modules/service/schema/user.dto.ts: -------------------------------------------------------------------------------- 1 | // Prepared from https://transform.tools/json-to-typescript 2 | 3 | export type User = { 4 | id: string; 5 | externalIds: string[]; 6 | rootOrgId: string; 7 | firstName?: string; 8 | lastName?: string; 9 | userLocation?: UserLocation; 10 | roles?: string; 11 | userType?: UserType; 12 | customData?: any; 13 | }; 14 | 15 | export interface UserLocation { 16 | id: string; 17 | state: string; 18 | district: string; 19 | block: string; 20 | cluster: string; 21 | school: string; 22 | } 23 | 24 | export interface UserType { 25 | subType: any; 26 | type: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/common/retry.ts: -------------------------------------------------------------------------------- 1 | export async function waitFor(millSeconds: number) { 2 | return new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(''); 5 | }, millSeconds); 6 | }); 7 | } 8 | 9 | export async function retryPromiseWithDelay(promiseFunc, nthTry: number, delayTime: number) { 10 | try { 11 | const res = await promiseFunc(); 12 | return res; 13 | } catch (e) { 14 | if (nthTry === 1) { 15 | return Promise.reject(e); 16 | } 17 | console.log('retrying', nthTry, 'time'); 18 | await waitFor(delayTime); 19 | return retryPromiseWithDelay(promiseFunc, nthTry - 1, delayTime); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 AS install 2 | WORKDIR /app 3 | COPY package.json ./ 4 | RUN yarn config set ignore-engines true 5 | RUN yarn install 6 | 7 | FROM node:16 as build 8 | WORKDIR /app 9 | COPY prisma ./prisma/ 10 | RUN npx prisma generate 11 | RUN ls prisma 12 | RUN ls prisma/generated 13 | COPY --from=install /app/node_modules ./node_modules 14 | COPY . . 15 | RUN npm run build 16 | 17 | FROM node:16 18 | WORKDIR /app 19 | COPY --from=build /app/dist ./dist 20 | COPY --from=build /app/package*.json ./ 21 | COPY --from=build /app/prisma ./prisma 22 | COPY --from=build /app/node_modules ./node_modules 23 | EXPOSE 3002 24 | CMD [ "npm", "run", "start:migrate:prod" ] 25 | -------------------------------------------------------------------------------- /src/modules/secrets/types/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Secret, getSecretType } from '.'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | describe('check type of secret', () => { 5 | it('should check an empty object with SecretType', () => { 6 | const secret = {} as Secret; 7 | expect(getSecretType(secret)).toBe('Headers'); 8 | }); 9 | 10 | it('should check an empty object with SecretType', () => { 11 | const secret = { 12 | usernameHSM: 'a', 13 | passwordHSM: 'b', 14 | username2Way: 'c', 15 | password2Way: 'd', 16 | } as Secret; 17 | expect(getSecretType(secret)).toBe('WhatsappGupshup'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/conversation-logic.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConversationLogicService } from './conversation-logic.service'; 3 | 4 | describe.skip('ConversationLogicService', () => { 5 | let service: ConversationLogicService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ConversationLogicService], 10 | }).compile(); 11 | 12 | service = module.get(ConversationLogicService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/modules/transformer/transformer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConversationLogicService } from '../conversation-logic/conversation-logic.service'; 3 | 4 | describe.skip('ConversationLogicService', () => { 5 | let service: ConversationLogicService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ConversationLogicService], 10 | }).compile(); 11 | 12 | service = module.get(ConversationLogicService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/sunbird-telemetry/sunbird-telemetry.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SunbirdTelemetryController } from './sunbird-telemetry.controller'; 3 | 4 | describe('SunbirdTelemetryController', () => { 5 | let controller: SunbirdTelemetryController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [SunbirdTelemetryController], 10 | }).compile(); 11 | 12 | controller = module.get( 13 | SunbirdTelemetryController, 14 | ); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/console.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { MigrationService } from './migration/migration.service'; 4 | 5 | async function bootstrap() { 6 | const application = await NestFactory.createApplicationContext(AppModule); 7 | 8 | const command = process.argv[2]; 9 | console.log({ command }); 10 | 11 | switch (command) { 12 | case 'insert-data': 13 | const migrationService = application.get(MigrationService); 14 | await migrationService.insertData(); 15 | break; 16 | default: 17 | console.log('Command not found'); 18 | process.exit(1); 19 | } 20 | 21 | await application.close(); 22 | process.exit(0); 23 | } 24 | 25 | bootstrap(); 26 | -------------------------------------------------------------------------------- /src/health/health.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | import { HttpModule } from '@nestjs/axios'; 4 | import { HealthController } from './health.controller'; 5 | import { HealthService } from './health.service'; 6 | import { PrismaService } from '../global-services/prisma.service'; 7 | import { FormService } from '../modules/form/form.service'; 8 | import { TelemetryService } from '../global-services/telemetry.service'; 9 | 10 | @Module({ 11 | imports: [TerminusModule, HttpModule], 12 | controllers: [HealthController], 13 | providers: [ 14 | HealthService, 15 | PrismaService, 16 | FormService, 17 | TelemetryService 18 | ], 19 | }) 20 | export class HealthModule {} 21 | -------------------------------------------------------------------------------- /src/modules/form/form.types.ts: -------------------------------------------------------------------------------- 1 | function withDefaults() { 2 | return function >(defs: TDefaults) { 3 | return function ( 4 | p: Pick> & Partial, 5 | ): T { 6 | const result: any = p; 7 | for (const k of Object.keys(defs)) { 8 | result[k] = result[k] || defs[k]; 9 | } 10 | return result; 11 | }; 12 | }; 13 | } 14 | 15 | export type FormUploadStatus = { 16 | status: 'PENDING' | 'UPLOADED' | 'ERROR'; 17 | error?: string; 18 | errorCode?: string; 19 | errorMessage?: string; 20 | data?: any; 21 | }; 22 | 23 | export type FormMediaUploadStatus = { 24 | status: 'PENDING' | 'UPLOADED' | 'ERROR'; 25 | error?: string; 26 | errorCode?: number; 27 | data?: any; 28 | } 29 | -------------------------------------------------------------------------------- /prisma/migrations/20220327072038_fix_remove_owner_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `ownerID` on the `ConversationLogic` table. All the data in the column will be lost. 5 | - You are about to drop the column `ownerOrgID` on the `ConversationLogic` table. All the data in the column will be lost. 6 | - You are about to drop the column `ownerID` on the `UserSegment` table. All the data in the column will be lost. 7 | - You are about to drop the column `ownerOrgID` on the `UserSegment` table. All the data in the column will be lost. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "ConversationLogic" DROP COLUMN "ownerID", 12 | DROP COLUMN "ownerOrgID"; 13 | 14 | -- AlterTable 15 | ALTER TABLE "UserSegment" DROP COLUMN "ownerID", 16 | DROP COLUMN "ownerOrgID"; 17 | -------------------------------------------------------------------------------- /src/modules/bot/dto/create-bot.dto.ts: -------------------------------------------------------------------------------- 1 | import { BotStatus } from "prisma/generated/prisma-client-js"; 2 | 3 | const exampleForDTO = { 4 | startingMessage: 'Hi Test Bot - 6', 5 | name: 'Test Bot - 6', 6 | tags: ['tag1', 'tag2'], 7 | users: ['8866c239-3cd9-498a-94b4-97f2696a83ec'], 8 | logic: ['e267d86f-fdd7-4fd5-ab37-2891e68393e6'], 9 | status: 'enabled', 10 | startDate: '2022-07-29', 11 | endDate: '2023-07-05', 12 | }; 13 | 14 | export class CreateBotDto { 15 | startingMessage: string; 16 | name: string; 17 | tags: string[]; 18 | users: string[]; 19 | logic: string[]; 20 | status: BotStatus; 21 | startDate: string; 22 | endDate: string; 23 | ownerid: string; 24 | ownerorgid: string; 25 | purpose: string | undefined; 26 | description: string | undefined; 27 | meta: Record | undefined; 28 | } 29 | -------------------------------------------------------------------------------- /src/common/file-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | interface FileMapper { 4 | file: Express.Multer.File; 5 | req: Request; 6 | } 7 | 8 | interface FilesMapper { 9 | files: Express.Multer.File[]; 10 | req: Request; 11 | } 12 | 13 | export const fileMapper = ({ file, req }: FileMapper) => { 14 | const image_url = `${req.protocol}://${req.headers.host}/${file.path}`; 15 | return { 16 | originalname: file.originalname, 17 | filename: file.filename, 18 | image_url, 19 | }; 20 | }; 21 | 22 | export const filesMapper = ({ files, req }: FilesMapper) => { 23 | return files.map((file) => { 24 | const image_url = `${req.protocol}://${req.headers.host}/${file.path}`; 25 | return { 26 | originalname: file.originalname, 27 | filename: file.filename, 28 | image_url, 29 | }; 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/secrets/secrets.service.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import VaultClient = require('node-vault-client'); 4 | 5 | @Injectable() 6 | export class VaultClientProvider { 7 | constructor( 8 | private configService: ConfigService 9 | ){} 10 | 11 | getClient(): any { 12 | const vaultAddress = this.configService.get('VAULT_ADDR'); 13 | const vaultToken = this.configService.get('VAULT_TOKEN'); 14 | 15 | if (!vaultAddress || !vaultToken) { 16 | throw new Error('Vault address and token are missing in the environment variables'); 17 | } 18 | 19 | return new VaultClient({ 20 | api: { url: vaultAddress }, 21 | auth: { 22 | type: 'token', 23 | config: { token: vaultToken }, 24 | }, 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /src/auth/auth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { ExtractJwt, Strategy } from 'passport-jwt'; 5 | import { AuthHelper } from './auth.helper'; 6 | import { User } from './user.entity'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 10 | @Inject(AuthHelper) 11 | private readonly helper: AuthHelper; 12 | 13 | constructor(@Inject(ConfigService) config: ConfigService) { 14 | super({ 15 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 16 | secretOrKey: config.get('AUTH_PUBLIC_KEY'), 17 | ignoreExpiration: true, 18 | }); 19 | } 20 | 21 | private validate(payload: string): Promise { 22 | return Promise.resolve(this.helper.validateUser(payload)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auth/sunbird.jwt.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "jti": "98ee456c-3b18-4df8-a546-d2fafc4aff40", 3 | "exp": 1632331428, 4 | "nbf": 0, 5 | "iat": 1632288228, 6 | "iss": "https://dev.sunbirded.org/auth/realms/sunbird", 7 | "aud": "account", 8 | "sub": "f:5a8a3f2b-3409-42e0-9001-f913bc0fde31:95e4942d-cbe8-477d-aebd-ad8e6de4bfc8", 9 | "typ": "Bearer", 10 | "azp": "project-sunbird-dev-client", 11 | "auth_time": 0, 12 | "session_state": "3042ef33-b4ff-48eb-bd0c-502fa369d1dd", 13 | "acr": "1", 14 | "allowed-origins": ["https://dev.sunbirded.org"], 15 | "realm_access": { 16 | "roles": ["offline_access", "uma_authorization"] 17 | }, 18 | "resource_access": { 19 | "account": { 20 | "roles": ["manage-account", "manage-account-links", "view-profile"] 21 | } 22 | }, 23 | "scope": "", 24 | "name": "Reviewer", 25 | "given_name": "Reviewer", 26 | "email": "us****@yopmail.com" 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/user-segment/fusionauth/fusionauthClientProvider.ts: -------------------------------------------------------------------------------- 1 | import FusionAuthClient from "@fusionauth/typescript-client"; 2 | import { Injectable, InternalServerErrorException } from "@nestjs/common"; 3 | import { ConfigService } from "@nestjs/config"; 4 | 5 | @Injectable() 6 | export class FusionAuthClientProvider { 7 | client: FusionAuthClient; 8 | 9 | constructor( 10 | private readonly configService: ConfigService, 11 | ) { 12 | this.client = new FusionAuthClient( 13 | configService.get('FUSIONAUTH_KEY') as string, 14 | configService.get('FUSIONAUTH_URL') as string, 15 | ); 16 | } 17 | 18 | getClient(): FusionAuthClient { 19 | if (!this.client) { 20 | throw new InternalServerErrorException('FusionAuthClient could not be initialized!'); 21 | } 22 | return this.client; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/health/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get } from '@nestjs/common'; 2 | import { HealthService } from './health.service'; 3 | import { HealthCheckResult } from '@nestjs/terminus'; 4 | import { Public } from '../auth/public.decorator'; 5 | 6 | @Controller('health') 7 | export class HealthController { 8 | constructor( 9 | private readonly healthService: HealthService 10 | ) {} 11 | 12 | @Public() 13 | @Get() 14 | async checkHealth(@Body() body: any): Promise { 15 | return this.healthService.checkHealth(); 16 | } 17 | 18 | @Public() 19 | @Get('/ping') 20 | async getServiceHealth(): Promise { 21 | const resp: HealthCheckResult = { 22 | status: 'ok', 23 | details: { 24 | "UCI-API": { 25 | "status": "up" 26 | } 27 | }, 28 | }; 29 | return resp; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/interceptors/addAdminHeader.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { ConfigService } from '@nestjs/config'; 9 | 10 | // Nestjs Lifecyle - https://i.stack.imgur.com/2lFhd.jpg 11 | @Injectable() 12 | export class AddAdminHeaderInterceptor implements NestInterceptor { 13 | constructor(private readonly config: ConfigService) {} 14 | 15 | intercept(context: ExecutionContext, next: CallHandler): Observable { 16 | const req = context.switchToHttp().getRequest(); 17 | const token = req.headers['admin-token']; 18 | if (this.config.get('ADMIN_TOKEN') === token && token !== undefined) { 19 | req.body.isAdmin = true; 20 | } else { 21 | req.body.isAdmin = false; 22 | } 23 | // for testing 24 | context.switchToHttp().getRequest().body = req.body; 25 | return next.handle(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { JwtAuthGuard } from './auth.guard'; 5 | import { APP_GUARD } from '@nestjs/core'; 6 | import { JwtStrategy } from './auth.strategy'; 7 | import { AuthHelper } from './auth.helper'; 8 | import { ConfigService } from '@nestjs/config'; 9 | 10 | @Module({ 11 | imports: [ 12 | JwtModule.registerAsync({ 13 | inject: [ConfigService], 14 | useFactory: (config: ConfigService) => ({ 15 | publicKey: `-----BEGIN PUBLIC KEY-----\n${config.get( 16 | 'AUTH_PUBLIC_KEY', 17 | )}\n-----END PUBLIC KEY-----`, 18 | algorithm: 'RS256', 19 | }), 20 | }), 21 | ], 22 | providers: [ 23 | AuthService, 24 | AuthHelper, 25 | { 26 | provide: APP_GUARD, 27 | useClass: JwtAuthGuard, 28 | }, 29 | JwtStrategy, 30 | ], 31 | }) 32 | export class AuthModule {} 33 | -------------------------------------------------------------------------------- /prisma/migrations/20220203070524_tst_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Post" ( 3 | "id" UUID NOT NULL DEFAULT gen_random_uuid(), 4 | 5 | CONSTRAINT "Post_pkey" PRIMARY KEY ("id") 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "Category" ( 10 | "id" UUID NOT NULL DEFAULT gen_random_uuid(), 11 | 12 | CONSTRAINT "Category_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateTable 16 | CREATE TABLE "_CategoryToPost" ( 17 | "A" UUID NOT NULL, 18 | "B" UUID NOT NULL 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "_CategoryToPost_AB_unique" ON "_CategoryToPost"("A", "B"); 23 | 24 | -- CreateIndex 25 | CREATE INDEX "_CategoryToPost_B_index" ON "_CategoryToPost"("B"); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "_CategoryToPost" ADD FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "_CategoryToPost" ADD FOREIGN KEY ("B") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; 32 | -------------------------------------------------------------------------------- /src/modules/service/service.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { TelemetryService } from '../../global-services/telemetry.service'; 4 | import { SecretsService } from '../secrets/secrets.service'; 5 | import { GQLResolverService } from './gql.resolver'; 6 | import { GetRequestResolverService } from './http-get.resolver'; 7 | import { PostRequestResolverService } from './http-post.resolver'; 8 | import { ServiceController } from './service.controller'; 9 | import { ServiceService } from './service.service'; 10 | import { VaultClientProvider } from '../secrets/secrets.service.provider'; 11 | 12 | @Module({ 13 | controllers: [ServiceController], 14 | providers: [ 15 | ServiceService, 16 | GQLResolverService, 17 | ConfigService, 18 | TelemetryService, 19 | SecretsService, 20 | GetRequestResolverService, 21 | PostRequestResolverService, 22 | VaultClientProvider, 23 | ], 24 | }) 25 | export class ServiceModule {} -------------------------------------------------------------------------------- /src/modules/transformer/dto/create-transformer.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | "name": "Mission Prerna Form", 4 | "transformers": [ 5 | { 6 | "id": "02f010b8-29ce-41e5-be3c-798536a2818b" 7 | }, 8 | { 9 | "id": "18cbe155-c6dc-4d17-b85e-00d0b62439ac", 10 | "meta": { 11 | "form": "https://hosted.my.form.here.com", 12 | "formID": "ss_form_mpc" 13 | } 14 | } 15 | ], 16 | "adapter": "44a9df72-3d7a-4ece-94c5-98cf26307324" 17 | } 18 | */ 19 | 20 | export type CreateTransformerDto = { 21 | id: string; 22 | } 23 | 24 | export interface CreateFormTransformerDto extends CreateTransformerDto { 25 | id: string; 26 | meta?: { 27 | form?: string; 28 | formID?: string; 29 | }; 30 | } 31 | 32 | export interface CreateConversationLogicDto { 33 | name: string; 34 | transformers: CreateTransformerDto[]; 35 | adapter: string; 36 | 37 | } -------------------------------------------------------------------------------- /.github/workflows/CI-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | Tests: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 16 19 | 20 | - name: Install Dependencies 21 | run: yarn config set ignore-engines true && yarn install 22 | 23 | - name: Generate Prisma Client and Test 24 | run: npx prisma generate && yarn test 2>&1 | tee test-report.txt 25 | continue-on-error: true 26 | env: 27 | PSQL_DB_URL: postgresql://john_doe:secretpassword@localhost:5432/mydatabase 28 | 29 | - name: Check Test Results 30 | run: | 31 | if grep -qi "Test Suites: .* failed" test-report.txt; then 32 | echo "Tests have failed." 33 | exit 1 34 | else 35 | echo "Tests have passed." 36 | fi 37 | -------------------------------------------------------------------------------- /src/interceptors/addOwnerInfo.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from '../global-services/prisma.service'; 3 | import { AddOwnerInfoInterceptor } from './addOwnerInfo.interceptor'; 4 | 5 | describe('VerifyOwnerMiddleware', () => { 6 | let middleware: AddOwnerInfoInterceptor; 7 | let prisma: PrismaService; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | providers: [AddOwnerInfoInterceptor, PrismaService], 12 | }).compile(); 13 | 14 | middleware = module.get(AddOwnerInfoInterceptor); 15 | prisma = module.get(PrismaService); 16 | }); 17 | 18 | it('should verify request as authorized"', async () => { 19 | prisma.bot.count = jest.fn().mockResolvedValue(1); 20 | 21 | prisma.bot.findUnique = jest 22 | .fn() 23 | .mockReturnValueOnce([ 24 | { id: '95244f48-b583-11ec-b909-0242ac120002', name: 'TestBot' }, 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/dto/create-conversation-logic.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | "name": "Mission Prerna Form", 4 | "transformers": [ 5 | { 6 | "id": "02f010b8-29ce-41e5-be3c-798536a2818b" 7 | }, 8 | { 9 | "id": "18cbe155-c6dc-4d17-b85e-00d0b62439ac", 10 | "meta": { 11 | "form": "https://hosted.my.form.here.com", 12 | "formID": "ss_form_mpc" 13 | } 14 | } 15 | ], 16 | "adapter": "44a9df72-3d7a-4ece-94c5-98cf26307324" 17 | } 18 | */ 19 | 20 | export type CreateTransformerDto = { 21 | id: string; 22 | }; 23 | 24 | export interface CreateFormTransformerDto extends CreateTransformerDto { 25 | id: string; 26 | meta?: { 27 | form?: string; 28 | formID?: string; 29 | }; 30 | } 31 | 32 | export interface CreateConversationLogicDto { 33 | id?: string; 34 | name: string; 35 | description?: string; 36 | transformers: CreateTransformerDto[]; 37 | adapter: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/service/types.ts: -------------------------------------------------------------------------------- 1 | import { User } from './schema/user.dto'; 2 | 3 | export type GqlConfig = { 4 | url: string; 5 | query: string; 6 | gql?: string; 7 | cadence: { 8 | perPage: number; 9 | retries: number; 10 | timeout: number; 11 | concurrent: boolean; 12 | pagination: boolean; 13 | 'retries-interval': number; 14 | }; 15 | pageParam: string; 16 | credentials: { 17 | vault: string; 18 | variable: string; 19 | }; 20 | verificationParams: { 21 | phone?: string; 22 | id?: string; 23 | }; 24 | errorNotificationWebhook?: string; 25 | }; 26 | 27 | export enum ErrorType { 28 | UserSchemaMismatch = 'UserSchemaMismatch', 29 | UserNotFound = 'UserNotFound', 30 | } 31 | 32 | export type GqlResolverError = { 33 | errorType: ErrorType; 34 | error: Error; 35 | user?: User; 36 | }; 37 | 38 | export type GetRequestConfig = GqlConfig; 39 | export type GetRequestResolverError = GqlResolverError; 40 | 41 | export type PostRequestConfig = GqlConfig & { requestBody?: any }; 42 | export type PostRequestResolverError = GqlConfig; 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "vsicons.associations.files": [ 4 | { 5 | "icon": "nest_interceptor_ts", 6 | "extensions": ["interceptor.ts"] 7 | }, 8 | { 9 | "icon": "nest_controller_ts", 10 | "extensions": ["controller.ts"] 11 | }, 12 | { 13 | "icon": "nest_module_ts", 14 | "extensions": ["module.ts"] 15 | }, 16 | { 17 | "icon": "nest_service_ts", 18 | "extensions": ["service.ts"] 19 | }, 20 | { 21 | "icon": "nest_guard_ts", 22 | "extensions": ["guard.ts"] 23 | }, 24 | { 25 | "icon": "nest_guard_ts", 26 | "extensions": ["guard.ts"] 27 | }, 28 | { 29 | "icon": "nest_guard_ts", 30 | "extensions": ["guard.ts"] 31 | }, 32 | { 33 | "icon": "prisma", 34 | "extensions": ["prisma"] 35 | } 36 | ], 37 | "[javascript]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "files.associations": { 41 | "*.env.*": "env", 42 | ".env": "env" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/user-segment/user-segment.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { UserSegmentService } from './user-segment.service'; 11 | 12 | @Controller('user-segment') 13 | export class UserSegmentController { 14 | constructor(private readonly userSegmentService: UserSegmentService) {} 15 | 16 | @Post() 17 | create(@Body() createUserSegmentDto: any) { 18 | return this.userSegmentService.create(createUserSegmentDto); 19 | } 20 | 21 | @Get() 22 | findAll() { 23 | return this.userSegmentService.findAll(); 24 | } 25 | 26 | @Get(':id') 27 | findOne(@Param('id') id: string) { 28 | return this.userSegmentService.findOne(id); 29 | } 30 | 31 | @Patch(':id') 32 | update(@Param('id') id: string, @Body() updateUserSegmentDto: any) { 33 | return this.userSegmentService.update(id, updateUserSegmentDto); 34 | } 35 | 36 | @Delete(':id') 37 | remove(@Param('id') id: string) { 38 | return this.userSegmentService.remove(id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/docker-push.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | tags: 6 | ["v*.*.*", "v*.*.*-*"] 7 | 8 | env: 9 | REGISTRY: docker.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build: 14 | name: Push Docker image to Docker Hub 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 40 17 | steps: 18 | - name: Check out the repo 19 | uses: actions/checkout@v3 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v2 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 24 | with: 25 | username: ${{secrets.DOCKER_USER}} 26 | password: ${{secrets.DOCKER_TOKEN}} 27 | - name: Set output 28 | id: vars 29 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 30 | - name: Build and push Docker image 31 | uses: docker/build-push-action@v2 32 | with: 33 | context: "." 34 | push: true 35 | tags: samagragovernance/uci-apis:${{ steps.vars.outputs.tag }}, latest -------------------------------------------------------------------------------- /src/interceptors/addROToResponse.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | NotFoundException, 7 | } from '@nestjs/common'; 8 | import { v4 as uuidv4 } from 'uuid'; 9 | import { map, Observable } from 'rxjs'; 10 | 11 | // Nestjs Lifecyle - https://i.stack.imgur.com/2lFhd.jpg 12 | 13 | export interface Response { 14 | data: T; 15 | } 16 | 17 | /** 18 | * @description 19 | * Adds response object created in addResponseObject.interceptor.ts to the response body. 20 | */ 21 | @Injectable() 22 | export class AddROToResponseInterceptor 23 | implements NestInterceptor> 24 | { 25 | intercept( 26 | context: ExecutionContext, 27 | next: CallHandler, 28 | ): Observable> { 29 | const req = context.switchToHttp().getRequest(); 30 | const rspObj = req.body.respObj; 31 | return next.handle().pipe( 32 | map((data) => { 33 | rspObj.result = data; 34 | rspObj.endTime = new Date(); 35 | if (data?.status === 404) throw new NotFoundException(rspObj); 36 | return rspObj; 37 | }), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/todo.md: -------------------------------------------------------------------------------- 1 | ### P1 2 | 3 | 1. Routes with Basic Tests 4 | - Secrets 5 | - ~~Create a secret with ownerInfo added~~ 6 | - ~~Delete all secrets~~ 7 | - Update a specific secret 8 | - Adapter 9 | - Service 10 | - UserSegment 11 | - Transformer 12 | - Conversation Logic 13 | - Bot 14 | - Forms (Migrate the existing form management code) 15 | 2. Create a new Postman collection 16 | 3. Onboard everyone to the new APIs 17 | 4. Add a script for migration to new DB structure 18 | 5. Github CI setup 19 | - Build a docker image 20 | - Publish a docker image to NPM 21 | - Code Coverage and Badges 22 | - Unit Tests 23 | - Integration Tests (e2e) with mocked DB 24 | - Integration Tests (e2e) with docker-compose based DB and inserted data 25 | - Deploy the docs as Github Pages 26 | 6. Create a link for Swagger docs on Github pages 27 | 28 | ### P2 29 | 30 | 2. Extract Service, Secrets as modules outside of the current repo as libraries 31 | 1. Github PR setup 32 | - Template for a PR 33 | 1. Create Good first issues 34 | 1. Create a new repo for Vault config and build for Sunbird 35 | 1. Add JS Docs for most folders 36 | -------------------------------------------------------------------------------- /src/auth/auth.helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | HttpException, 4 | HttpStatus, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { User } from './user.entity'; 9 | 10 | @Injectable() 11 | export class AuthHelper { 12 | private readonly jwt: JwtService; 13 | 14 | constructor(jwt: JwtService) { 15 | this.jwt = jwt; 16 | } 17 | 18 | // Decoding the JWT Token 19 | public async decode(token: string): Promise { 20 | return this.jwt.decode(token); 21 | } 22 | 23 | // Validate JWT Token, throw forbidden error if JWT Token is invalid 24 | private async validate(token: string): Promise { 25 | const decoded: any = this.jwt.verify(token); 26 | 27 | if (!decoded) { 28 | throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); 29 | } 30 | 31 | return ( 32 | decoded['ownerId'] === undefined && decoded['ownerOrgId'] === undefined 33 | ); 34 | } 35 | 36 | validateUser(payload: string): User { 37 | return { 38 | ownerId: payload['ownerId'], 39 | ownerOrgId: payload['ownerOrgId'], 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/migration/data/getAdapterQuery.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "adapter": [ 4 | { 5 | "channel": "WhatsApp", 6 | "config": { 7 | "2WAY": "2000193033", 8 | "phone": "9876543210", 9 | "HSM_ID": "2000193031", 10 | "credentials": { 11 | "vault": "samagra", 12 | "variable": "gupshupSamagraProd" 13 | } 14 | }, 15 | "created_at": "2021-06-16T06:02:41.823863+00:00", 16 | "id": "44a9df72-3d7a-4ece-94c5-98cf26307324", 17 | "name": "SamagraProd", 18 | "provider": "gupshup", 19 | "updated_at": "2021-06-16T06:02:39.125273+00:00" 20 | }, 21 | { 22 | "channel": "WhatsApp", 23 | "config": { 24 | "phone": "912249757677", 25 | "credentials": { "vault": "samagra", "variable": "netcoreUAT" } 26 | }, 27 | "created_at": "2021-06-16T06:02:41.823863+00:00", 28 | "id": "44a9df72-3d7a-4ece-94c5-98cf26307323", 29 | "name": "SamagraNetcoreUAT", 30 | "provider": "Netcore", 31 | "updated_at": "2021-06-16T06:02:39.125273+00:00" 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /prisma/migrations/20220327160740_usersegment_services_optional/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "UserSegment" DROP CONSTRAINT "UserSegment_allServiceID_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "UserSegment" DROP CONSTRAINT "UserSegment_byIDServiceID_fkey"; 6 | 7 | -- DropForeignKey 8 | ALTER TABLE "UserSegment" DROP CONSTRAINT "UserSegment_byPhoneServiceID_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "UserSegment" ALTER COLUMN "allServiceID" DROP NOT NULL, 12 | ALTER COLUMN "byPhoneServiceID" DROP NOT NULL, 13 | ALTER COLUMN "byIDServiceID" DROP NOT NULL; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "UserSegment" ADD CONSTRAINT "UserSegment_allServiceID_fkey" FOREIGN KEY ("allServiceID") REFERENCES "Service"("id") ON DELETE SET NULL ON UPDATE CASCADE; 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "UserSegment" ADD CONSTRAINT "UserSegment_byIDServiceID_fkey" FOREIGN KEY ("byIDServiceID") REFERENCES "Service"("id") ON DELETE SET NULL ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "UserSegment" ADD CONSTRAINT "UserSegment_byPhoneServiceID_fkey" FOREIGN KEY ("byPhoneServiceID") REFERENCES "Service"("id") ON DELETE SET NULL ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /src/common/prismaError.handler.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '../../prisma/generated/prisma-client-js'; 2 | import { PrismaError } from './prismaError'; 3 | 4 | export const stackTraceParser = (stackTrace: string | undefined): string[] => { 5 | if (stackTrace === undefined) return []; 6 | return stackTrace.split('\n'); 7 | }; 8 | 9 | export const prismaErrorHandler = (error: any): PrismaError => { 10 | const parsedStackTrace = error.message.split('\n'); 11 | const errorMessage = parsedStackTrace[parsedStackTrace.length - 1].trim(); 12 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 13 | switch (error.code) { 14 | case 'P2002': 15 | return { 16 | code: error.code, 17 | message: errorMessage, 18 | stackTrace: stackTraceParser(error.stack), 19 | }; 20 | default: 21 | return { 22 | code: 'UNIDENTIFIED', 23 | message: 'Not Implemented', 24 | stackTrace: [] as string[], 25 | }; 26 | } 27 | } else { 28 | return { 29 | code: 'UNIDENTIFIED', 30 | message: 'Not Implemented', 31 | stackTrace: [] as string[], 32 | }; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/modules/secrets/types/index.ts: -------------------------------------------------------------------------------- 1 | import * as HasuraSecret from './hasura.secret'; 2 | import * as Headers from './headers.secret'; 3 | import * as WhatsappGupshupSecret from './whatsapp.gupshup.secret'; 4 | import * as WhatsappNetcoreSecret from './whatsapp.netcore.secrets'; 5 | 6 | export type Secret = 7 | | WhatsappGupshupSecret.default 8 | | WhatsappNetcoreSecret.default 9 | | HasuraSecret.default 10 | | Headers.default; 11 | 12 | export enum SecretType { 13 | WhatsappGupshup = 'WhatsappGupshup', 14 | WhatsappNetcore = 'WhatsappNetcore', 15 | Hasura = 'Hasura', 16 | Headers = 'Headers', 17 | } 18 | 19 | // Create a type guard for Secret 20 | export function getSecretType(secret: Secret): SecretType | null { 21 | if ( 22 | 'usernameHSM' in secret && 23 | 'passwordHSM' in secret && 24 | 'username2Way' in secret && 25 | 'password2Way' in secret 26 | ) { 27 | return SecretType.WhatsappGupshup; 28 | } 29 | if ('baseURL' in secret && 'adminSecret' in secret) { 30 | return SecretType.Hasura; 31 | } 32 | if ('source' in secret && 'bearer' in secret) { 33 | return SecretType.WhatsappNetcore; 34 | } 35 | return SecretType.Headers; 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/transformer/transformer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Transformer, Service } from '../../../prisma/generated/prisma-client-js'; 3 | import { PrismaService } from '../../global-services/prisma.service'; 4 | 5 | @Injectable() 6 | export class TransformerService { 7 | constructor(private prisma: PrismaService) { } 8 | create(data: any): Promise { 9 | const createData = { 10 | service: { create: data.service }, 11 | name: data.name, 12 | tags: data.tags, 13 | config: data.config, 14 | } 15 | return this.prisma.transformer.create({ data: createData }); 16 | } 17 | 18 | findAll(): Promise { 19 | return this.prisma.transformer.findMany(); 20 | } 21 | 22 | findOne(id: string): Promise { 23 | return this.prisma.transformer.findUnique({ where: { id } }); 24 | } 25 | 26 | update(id: string, updateAdapterDto: any) { 27 | return this.prisma.transformer.update({ 28 | where: { 29 | id, 30 | }, 31 | data: updateAdapterDto, 32 | }); 33 | } 34 | 35 | remove(id) { 36 | return `This action removes a #${id} adapter`; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/global-services/telemetry.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; 2 | import PostHog from 'posthog-node'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Injectable() 6 | export class TelemetryService implements OnModuleInit { 7 | private readonly logger = new Logger('TelemetryService'); 8 | client: PostHog; 9 | constructor(private configService: ConfigService) { 10 | this.client = new PostHog(this.configService.get('POSTHOG_API_KEY') || '', { 11 | host: configService.get('POSTHOG_API_HOST'), 12 | flushAt: configService.get('POSTHOG_BATCH_SIZE'), 13 | flushInterval: configService.get('POSTHOG_FLUSH_INTERVAL'), 14 | }); 15 | } 16 | 17 | async onModuleInit() { 18 | // This should only be printed once - https://docs.nestjs.com/assets/lifecycle-events.png 19 | this.logger.verbose('Initialized Successfully 🎉'); 20 | this.client.identify({ 21 | distinctId: 'NestJS-Local', 22 | properties: { 23 | version: this.configService.get('NEST_VERSION'), 24 | }, 25 | }); 26 | } 27 | 28 | async beforeApplicationShutdown() { 29 | await this.client.shutdown(); 30 | this.logger.verbose('Gracefully Shutdown 🎉'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { PrismaService } from './global-services/prisma.service'; 5 | 6 | class PrismaServiceMock { 7 | bots = { 8 | count: jest.fn().mockResolvedValue(42), 9 | }; 10 | } 11 | 12 | describe('AppController', () => { 13 | let appController: AppController; 14 | let appService: AppService; 15 | 16 | beforeEach(async () => { 17 | const app: TestingModule = await Test.createTestingModule({ 18 | controllers: [AppController], 19 | providers: [ 20 | AppService, 21 | { provide: PrismaService, useClass: PrismaServiceMock }, 22 | ], 23 | }).compile(); 24 | 25 | appController = app.get(AppController); 26 | appService = app.get(AppService); 27 | }); 28 | 29 | describe('/getBotCount', () => { 30 | it('should return the bot count from AppService', async () => { 31 | const mockBotCount = 42; 32 | jest.spyOn(appService, 'getBotCount').mockResolvedValue(mockBotCount); 33 | const response = await appController.getBotCount(); 34 | expect(response).toEqual({ data: mockBotCount }); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import supertest from 'supertest'; 2 | 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { AppModule } from './../src/app.module'; 6 | import { INestApplication } from '@nestjs/common'; 7 | import { PrismaService } from './../src/global-services/prisma.service'; 8 | 9 | describe('AppController (e2e)', () => { 10 | let app: INestApplication; 11 | let prisma: PrismaService; 12 | 13 | beforeEach(async () => { 14 | const moduleFixture: TestingModule = await Test.createTestingModule({ 15 | imports: [AppModule], 16 | }).compile(); 17 | 18 | app = moduleFixture.createNestApplication(); 19 | prisma = app.get(PrismaService); 20 | await app.init(); 21 | // add mock data to prisma 22 | }); 23 | 24 | afterAll(async () => { 25 | await prisma.truncate(); 26 | await prisma.resetSequences(); 27 | await prisma.$disconnect(); 28 | await app.close(); 29 | }); 30 | 31 | it('/ (GET)', () => { 32 | return supertest(app.getHttpServer()) 33 | .get('/') 34 | .expect(200) 35 | .expect('Hello World!'); 36 | }); 37 | 38 | it('/getBotCount (GET)', () => { 39 | return supertest(app.getHttpServer()) 40 | .get('/getBotCount') 41 | .expect(200) 42 | .expect({ data: 1 }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/sunbird-telemetry/sunbird-telemetry.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { MessagePattern } from '@nestjs/microservices'; 3 | import fetch from 'node-fetch'; 4 | 5 | interface IncomingMessage { 6 | topic: string; 7 | partition: number; 8 | timestamp: string; 9 | size: number; 10 | attributes: number; 11 | offset: string; 12 | key: any; 13 | value: any; 14 | headers: Record; 15 | } 16 | 17 | @Controller() 18 | export class SunbirdTelemetryController { 19 | @MessagePattern('telemetry') 20 | async handleEntityCreated(payload: IncomingMessage) { 21 | console.log(JSON.parse(payload.value)); 22 | const eventPayload = JSON.parse(payload.value); 23 | const event = { 24 | id: 'ekstep.telemetry', 25 | ver: '3.0', 26 | ets: Math.floor(Date.now() / 1000), 27 | events: [] as any[], 28 | }; 29 | event.events.push(eventPayload); 30 | // TODO: migrate this to undici 31 | const telemetryResponse = await fetch( 32 | process.env.TELEMETRY_BASE_URL + '/v1/telemetry', 33 | { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | body: JSON.stringify(event), 39 | }, 40 | ); 41 | console.log({ telemetryResponse }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/interceptors/multipleFiles.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import { Observable } from 'rxjs'; 11 | import FastifyMulter from 'fastify-multer'; 12 | import { Options, Multer } from 'multer'; 13 | 14 | type MulterInstance = any; 15 | export function FastifyFilesInterceptor( 16 | fieldName: string, 17 | maxCount?: number, 18 | localOptions?: Options, 19 | ): Type { 20 | class MixinInterceptor implements NestInterceptor { 21 | protected multer: MulterInstance; 22 | 23 | constructor( 24 | @Optional() 25 | @Inject('MULTER_MODULE_OPTIONS') 26 | options: Multer, 27 | ) { 28 | this.multer = (FastifyMulter as any)({ ...options, ...localOptions }); 29 | } 30 | 31 | async intercept( 32 | context: ExecutionContext, 33 | next: CallHandler, 34 | ): Promise> { 35 | const ctx = context.switchToHttp(); 36 | 37 | await new Promise((resolve, reject) => 38 | this.multer.array(fieldName, maxCount)( 39 | ctx.getRequest(), 40 | ctx.getResponse(), 41 | (error: any) => { 42 | if (error) { 43 | // const error = transformException(err); 44 | return reject(error); 45 | } 46 | resolve(); 47 | }, 48 | ), 49 | ); 50 | 51 | return next.handle(); 52 | } 53 | } 54 | const Interceptor = mixin(MixinInterceptor); 55 | return Interceptor as Type; 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/bot/bot.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BotService } from './bot.service'; 3 | import { BotController } from './bot.controller'; 4 | import { PrismaService } from '../../global-services/prisma.service'; 5 | import { ServiceService } from '../service/service.service'; 6 | import { GQLResolverService } from '../service/gql.resolver'; 7 | import { TelemetryService } from '../../global-services/telemetry.service'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { SecretsService } from '../secrets/secrets.service'; 10 | import { DeviceManagerService } from '../user-segment/fusionauth/fusionauth.service'; 11 | import { GetRequestResolverService } from '../service/http-get.resolver'; 12 | import { PostRequestResolverService } from '../service/http-post.resolver'; 13 | import { FusionAuthClientProvider } from '../user-segment/fusionauth/fusionauthClientProvider'; 14 | import { VaultClientProvider } from '../secrets/secrets.service.provider'; 15 | import { UserSegmentService } from '../user-segment/user-segment.service'; 16 | import { ConversationLogicService } from '../conversation-logic/conversation-logic.service'; 17 | 18 | @Module({ 19 | controllers: [BotController], 20 | providers: [ 21 | BotService, 22 | PrismaService, 23 | ServiceService, 24 | GQLResolverService, 25 | GetRequestResolverService, 26 | PostRequestResolverService, 27 | ConfigService, 28 | TelemetryService, 29 | SecretsService, 30 | FusionAuthClientProvider, 31 | DeviceManagerService, 32 | VaultClientProvider, 33 | UserSegmentService, 34 | ConversationLogicService, 35 | ], 36 | }) 37 | export class BotModule {} 38 | -------------------------------------------------------------------------------- /docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | federation-service: 5 | build: . 6 | image: uci-apis:0.0.1 7 | container_name: federation-service 8 | restart: unless-stopped 9 | volumes: 10 | - ./src/helpers/vaultDataMock.json:/opt/uci/src/helpers/vaultDataMock.json 11 | env_file: 12 | - ./src/.env 13 | ports: 14 | - 9998:9999 15 | 16 | uci-db: 17 | container_name: uci-db 18 | image: postgres:9.6 19 | env_file: 20 | - ./src/.env 21 | command: 'postgres -c max_connections=1000 -c shared_buffers=300MB' 22 | ports: 23 | - '15432:5432' 24 | volumes: 25 | - ./pgdata:/var/lib/postgresql/data 26 | 27 | gql: 28 | image: fedormelexin/graphql-engine-arm64:latest 29 | env_file: 30 | - ./src/.env 31 | ports: 32 | - '15003:8080' 33 | depends_on: 34 | - uci-db 35 | restart: always 36 | 37 | scheduler-db: 38 | container_name: scheduler-db 39 | image: redis:latest 40 | ports: 41 | - '6379:6379' 42 | command: ['redis-server', '--appendonly', 'yes'] 43 | hostname: redis 44 | volumes: 45 | - ./redis-data:/data 46 | - ./redis.conf:/usr/local/etc/redis/redis.conf 47 | 48 | vault: 49 | image: vault:latest 50 | volumes: 51 | - ./vault/config:/vault/config 52 | - ./vault/policies:/vault/policies 53 | - ./vault/data:/vault/data 54 | ports: 55 | - 8200:8200 56 | environment: 57 | - VAULT_ADDR=http://0.0.0.0:8200 58 | - VAULT_API_ADDR=http://0.0.0.0:8200 59 | - VAULT_ADDRESS=http://0.0.0.0:8200 60 | cap_add: 61 | - IPC_LOCK 62 | command: vault server -config=/vault/config/vault.json 63 | -------------------------------------------------------------------------------- /src/interceptors/multipleFields.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import { Observable } from 'rxjs'; 11 | import FastifyMulter from 'fastify-multer'; 12 | import { Field } from 'fastify-multer/lib/interfaces'; 13 | import { Options, Multer } from 'multer'; 14 | 15 | type MulterInstance = any; 16 | export function FastifyFileFieldInterceptor( 17 | fieldNames: Field[], 18 | localOptions: Options, 19 | ): Type { 20 | class MixinInterceptor implements NestInterceptor { 21 | protected multer: MulterInstance; 22 | 23 | constructor( 24 | @Optional() 25 | @Inject('MULTER_MODULE_OPTIONS') 26 | options: Multer, 27 | ) { 28 | this.multer = (FastifyMulter as any)({ ...options, ...localOptions }); 29 | } 30 | 31 | async intercept( 32 | context: ExecutionContext, 33 | next: CallHandler, 34 | ): Promise> { 35 | const ctx = context.switchToHttp(); 36 | 37 | await new Promise((resolve, reject) => 38 | this.multer.fields(fieldNames)( 39 | ctx.getRequest(), 40 | ctx.getResponse(), 41 | (error: any) => { 42 | if (error) { 43 | console.log(error); 44 | return reject(error); 45 | } 46 | resolve(); 47 | }, 48 | ), 49 | ); 50 | 51 | return next.handle(); 52 | } 53 | } 54 | const Interceptor = mixin(MixinInterceptor); 55 | return Interceptor as Type; 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/transformer/transformer.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseInterceptors, 10 | } from '@nestjs/common'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | import { TransformerService } from './transformer.service'; 13 | import { AddResponseObjectInterceptor } from '../../interceptors/addResponseObject.interceptor'; 14 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 15 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 16 | import { AddROToResponseInterceptor } from '../../interceptors/addROToResponse.interceptor'; 17 | 18 | @ApiTags('Transformer') 19 | @UseInterceptors( 20 | AddResponseObjectInterceptor, 21 | AddAdminHeaderInterceptor, 22 | AddOwnerInfoInterceptor, 23 | AddROToResponseInterceptor, 24 | ) 25 | @Controller({ 26 | path: 'transformer', 27 | }) 28 | export class TransformerController { 29 | constructor( 30 | private readonly transformerService: TransformerService, 31 | ) { } 32 | 33 | @Post() 34 | create(@Body() createTransformerDto: any) { 35 | return this.transformerService.create(createTransformerDto); 36 | } 37 | 38 | @Get() 39 | findAll() { 40 | return this.transformerService.findAll(); 41 | } 42 | 43 | @Get(':id') 44 | findOne(@Param('id') id: string) { 45 | return this.transformerService.findOne(id); 46 | } 47 | 48 | @Patch(':id') 49 | update(@Param('id') id: string, @Body() updateTransformerDto: any) { 50 | return this.transformerService.update(id, updateTransformerDto); 51 | } 52 | 53 | @Delete(':id') 54 | remove(@Param('id') id: string) { 55 | return this.transformerService.remove(id); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/interceptors/addResponseObject.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { v4 as uuidv4 } from 'uuid'; 8 | import { Observable } from 'rxjs'; 9 | import { getAppIdForResponse } from './utils/responseUtils'; 10 | 11 | // Nestjs Lifecyle - https://i.stack.imgur.com/2lFhd.jpg 12 | 13 | @Injectable() 14 | export class AddResponseObjectInterceptor implements NestInterceptor { 15 | intercept(context: ExecutionContext, next: CallHandler): Observable { 16 | const req = context.switchToHttp().getRequest(); 17 | req.body = req.body || {}; 18 | req.body.ts = new Date(); 19 | req.body.url = req.url; 20 | req.body.path = req.path; 21 | req.body.params = req.body.params ? req.body.params : {}; 22 | req.body.params.msgid = req.body.params.msgid || uuidv4(); 23 | req.body.id = req.body.params.msgid; 24 | const rspObj = { 25 | apiId: getAppIdForResponse(req.body.url), 26 | path: req.body.url, 27 | apiVersion: 'v1', 28 | msgid: req.body.params.msgid, 29 | result: {}, 30 | startTime: new Date(), 31 | method: req.method, 32 | did: req.headers['x-device-id'], 33 | }; 34 | 35 | const removedHeaders = [ 36 | 'host', 37 | 'origin', 38 | 'accept', 39 | 'referer', 40 | 'content-length', 41 | 'user-agent', 42 | 'accept-language', 43 | 'accept-charset', 44 | 'cookie', 45 | 'dnt', 46 | 'postman-token', 47 | 'cache-control', 48 | 'connection', 49 | ]; 50 | 51 | removedHeaders.forEach(function (e) { 52 | delete req.headers[e]; 53 | }); 54 | 55 | req.body.respObj = rspObj; 56 | return next.handle(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/global-services/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INestApplication, 3 | Injectable, 4 | Logger, 5 | OnModuleInit, 6 | } from '@nestjs/common'; 7 | 8 | import { PrismaClient } from '../../prisma/generated/prisma-client-js'; 9 | 10 | @Injectable() 11 | export class PrismaService extends PrismaClient implements OnModuleInit { 12 | private readonly logger = new Logger('DBService'); 13 | async onModuleInit() { 14 | this.logger.verbose('Initialized and Connected 🎉'); 15 | await this.$connect(); 16 | } 17 | 18 | async truncate() { 19 | const records = await this.$queryRawUnsafe>(`SELECT tablename 20 | FROM pg_tables 21 | WHERE schemaname = 'public'`); 22 | records.forEach((record) => this.truncateTable(record['tablename'])); 23 | } 24 | 25 | async truncateTable(tablename) { 26 | if (tablename === undefined || tablename === '_prisma_migrations') { 27 | return; 28 | } 29 | try { 30 | await this.$executeRawUnsafe( 31 | `TRUNCATE TABLE "public"."${tablename}" CASCADE;`, 32 | ); 33 | } catch (error) { 34 | console.log({ error }); 35 | } 36 | } 37 | 38 | async resetSequences() { 39 | const results = await this.$queryRawUnsafe>( 40 | `SELECT c.relname 41 | FROM pg_class AS c 42 | JOIN pg_namespace AS n ON c.relnamespace = n.oid 43 | WHERE c.relkind = 'S' 44 | AND n.nspname = 'public'`, 45 | ); 46 | for (const { record } of results) { 47 | // eslint-disable-next-line no-await-in-loop 48 | await this.$executeRawUnsafe( 49 | `ALTER SEQUENCE "public"."${record['relname']}" RESTART WITH 1;`, 50 | ); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/adapter/adapter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from '../../global-services/prisma.service'; 3 | import { AdapterDTO } from './dto'; 4 | import { Adapter, Prisma } from 'prisma/generated/prisma-client-js'; 5 | import { PrismaError } from 'src/common/prismaError'; 6 | import { TelemetryService } from '../../global-services/telemetry.service'; 7 | import { prismaErrorHandler } from '../../common/prismaError.handler'; 8 | 9 | @Injectable() 10 | export class AdaptersService { 11 | serviceName = 'Adapter'; 12 | methodCalled: string; 13 | constructor( 14 | private prisma: PrismaService, 15 | private telemetry: TelemetryService, 16 | ) {} 17 | async create(createData: AdapterDTO): Promise { 18 | this.methodCalled = 'create'; 19 | return this.prisma.adapter 20 | .create({ 21 | data: { 22 | channel: createData.channel, 23 | provider: createData.provider, 24 | name: createData.name, 25 | config: { 26 | ...createData.config, 27 | }, 28 | }, 29 | }) 30 | .catch((err: Prisma.PrismaClientKnownRequestError): PrismaError => { 31 | return prismaErrorHandler(err); 32 | }); 33 | } 34 | 35 | findAll(): Promise { 36 | return this.prisma.adapter.findMany(); 37 | } 38 | 39 | findOne(id: string): Promise { 40 | return this.prisma.adapter.findUnique({ where: { id } }); 41 | } 42 | 43 | update(id: string, updateAdapterDto: any) { 44 | return this.prisma.adapter.update({ 45 | where: { 46 | id, 47 | }, 48 | data: updateAdapterDto, 49 | }); 50 | } 51 | 52 | remove(id: number) { 53 | return `This action removes a #${id} adapter`; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /prisma/migrations/20220327080251_udpate_transformer_config_in_cl/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `_ConversationLogicToTransformer` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "_ConversationLogicToTransformer" DROP CONSTRAINT "_ConversationLogicToTransformer_A_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "_ConversationLogicToTransformer" DROP CONSTRAINT "_ConversationLogicToTransformer_B_fkey"; 12 | 13 | -- AlterTable 14 | ALTER TABLE "ConversationLogic" ADD COLUMN "transformerId" UUID; 15 | 16 | -- DropTable 17 | DROP TABLE "_ConversationLogicToTransformer"; 18 | 19 | -- CreateTable 20 | CREATE TABLE "TransformerConfig" ( 21 | "id" UUID NOT NULL DEFAULT gen_random_uuid(), 22 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | "updatedAt" TIMESTAMP(3) NOT NULL, 24 | "transformerId" UUID NOT NULL, 25 | "meta" JSONB NOT NULL, 26 | "conversationLogicId" UUID, 27 | 28 | CONSTRAINT "TransformerConfig_pkey" PRIMARY KEY ("id") 29 | ); 30 | 31 | -- CreateIndex 32 | CREATE UNIQUE INDEX "TransformerConfig_transformerId_key" ON "TransformerConfig"("transformerId"); 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "TransformerConfig" ADD CONSTRAINT "TransformerConfig_transformerId_fkey" FOREIGN KEY ("transformerId") REFERENCES "Transformer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 36 | 37 | -- AddForeignKey 38 | ALTER TABLE "TransformerConfig" ADD CONSTRAINT "TransformerConfig_conversationLogicId_fkey" FOREIGN KEY ("conversationLogicId") REFERENCES "ConversationLogic"("id") ON DELETE SET NULL ON UPDATE CASCADE; 39 | 40 | -- AddForeignKey 41 | ALTER TABLE "ConversationLogic" ADD CONSTRAINT "ConversationLogic_transformerId_fkey" FOREIGN KEY ("transformerId") REFERENCES "Transformer"("id") ON DELETE SET NULL ON UPDATE CASCADE; 42 | -------------------------------------------------------------------------------- /src/modules/service/service.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { Service } from 'prisma/generated/prisma-client-js'; 3 | import { ServiceQueryType } from './enum'; 4 | import { GQLResolverService } from './gql.resolver'; 5 | import { GetRequestResolverService } from './http-get.resolver'; 6 | import { GqlConfig } from './types'; 7 | 8 | @Injectable() 9 | export class ServiceService { 10 | logger: Logger; 11 | constructor( 12 | private readonly gqlResolver: GQLResolverService, 13 | private readonly getRequestResolver: GetRequestResolverService, 14 | ) { 15 | this.logger = new Logger('ServiceService'); 16 | } 17 | 18 | async resolve(service: Service, segment: number, page: number | undefined, owner: string | null, conversationToken: string) { 19 | const startTime = performance.now(); 20 | this.logger.log(`ServiceService::resolve: Resolving users. Page: ${page}`); 21 | if (service.type === 'gql') { 22 | const resp = await this.gqlResolver.resolve( 23 | ServiceQueryType.all, 24 | service.config as GqlConfig, 25 | owner, 26 | page, 27 | conversationToken 28 | ); 29 | this.logger.log(`ServiceService::resolve: Users resolved: ${resp.length}. Time taken: ${performance.now() - startTime}`); 30 | return resp; 31 | } else if (service.type === 'get') { 32 | const resp = await this.getRequestResolver.resolve( 33 | ServiceQueryType.all, 34 | service.config as GqlConfig, 35 | owner, 36 | segment, 37 | page, 38 | conversationToken 39 | ); 40 | this.logger.log(`ServiceService::resolve: Users resolved: ${resp.length}. Time taken: ${performance.now() - startTime}`); 41 | return resp; 42 | } else { 43 | this.logger.error(`Unknown service type: ${service.type}`); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/conversation-logic.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseInterceptors, 10 | } from '@nestjs/common'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | import { ConversationLogicService } from './conversation-logic.service'; 13 | import { AddResponseObjectInterceptor } from '../../interceptors/addResponseObject.interceptor'; 14 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 15 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 16 | import { AddROToResponseInterceptor } from '../../interceptors/addROToResponse.interceptor'; 17 | 18 | @ApiTags('ConversationLogic') 19 | @UseInterceptors( 20 | AddResponseObjectInterceptor, 21 | AddAdminHeaderInterceptor, 22 | AddOwnerInfoInterceptor, 23 | AddROToResponseInterceptor, 24 | ) 25 | @Controller({ 26 | path: 'conversationLogic', 27 | }) 28 | export class ConversationLogicController { 29 | constructor( 30 | private readonly conversationLogicService: ConversationLogicService, 31 | ) {} 32 | 33 | @Post() 34 | create(@Body() createConversationLogicDto: any) { 35 | return this.conversationLogicService.create( 36 | createConversationLogicDto.data, 37 | ); 38 | } 39 | 40 | @Get() 41 | findAll() { 42 | return this.conversationLogicService.findAll(); 43 | } 44 | 45 | @Get(':id') 46 | findOne(@Param('id') id: string) { 47 | return this.conversationLogicService.findOne(id); 48 | } 49 | 50 | @Patch(':id') 51 | update(@Param('id') id: string, @Body() updateConversationLogicDto: any) { 52 | return this.conversationLogicService.update(id, updateConversationLogicDto); 53 | } 54 | 55 | @Delete(':id') 56 | remove(@Param('id') id: string) { 57 | return this.conversationLogicService.remove(id); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/interceptors/file.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import { Observable } from 'rxjs'; 11 | import FastifyMulter from 'fastify-multer'; 12 | import { Options, Multer } from 'multer'; 13 | 14 | interface MultipartFile { 15 | toBuffer: () => Promise; 16 | file: NodeJS.ReadableStream; 17 | filepath: string; 18 | fieldname: string; 19 | filename: string; 20 | encoding: string; 21 | mimetype: string; 22 | fields: import('fastify-multipart').MultipartFields; 23 | } 24 | 25 | // interface FastifyRequest { 26 | // incomingFile: MultipartFile; 27 | // } 28 | 29 | type MulterInstance = any; 30 | export function FastifyFileInterceptor( 31 | fieldName: string, 32 | localOptions: Options, 33 | ): Type { 34 | class MixinInterceptor implements NestInterceptor { 35 | protected multer: MulterInstance; 36 | 37 | constructor( 38 | @Optional() 39 | @Inject('MULTER_MODULE_OPTIONS') 40 | options: Multer, 41 | ) { 42 | this.multer = (FastifyMulter as any)({ ...options, ...localOptions }); 43 | } 44 | 45 | async intercept( 46 | context: ExecutionContext, 47 | next: CallHandler, 48 | ): Promise> { 49 | const ctx = context.switchToHttp(); 50 | 51 | await new Promise((resolve, reject) => 52 | this.multer.single(fieldName)( 53 | ctx.getRequest(), 54 | ctx.getResponse(), 55 | (error: any) => { 56 | if (error) { 57 | // const error = transformException(err); 58 | console.log(error); 59 | return reject(error); 60 | } 61 | resolve(); 62 | }, 63 | ), 64 | ); 65 | 66 | return next.handle(); 67 | } 68 | } 69 | const Interceptor = mixin(MixinInterceptor); 70 | return Interceptor as Type; 71 | } 72 | -------------------------------------------------------------------------------- /src/modules/adapter/adapter.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | Version, 10 | Put, 11 | UseInterceptors, 12 | } from '@nestjs/common'; 13 | import { PrismaService } from '../../global-services/prisma.service'; 14 | import { AdaptersService } from './adapter.service'; 15 | import { ApiTags } from '@nestjs/swagger'; 16 | import { AdapterDTO } from './dto'; 17 | import { Adapter } from 'prisma/generated/prisma-client-js'; 18 | import { PrismaError } from 'src/common/prismaError'; 19 | import { AddResponseObjectInterceptor } from '../../interceptors/addResponseObject.interceptor'; 20 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 21 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 22 | import { AddROToResponseInterceptor } from '../../interceptors/addROToResponse.interceptor'; 23 | 24 | @ApiTags('Adapters') 25 | @UseInterceptors( 26 | AddResponseObjectInterceptor, 27 | AddAdminHeaderInterceptor, 28 | AddOwnerInfoInterceptor, 29 | AddROToResponseInterceptor, 30 | ) 31 | @Controller({ 32 | path: 'adapter', 33 | }) 34 | export class AdaptersController { 35 | constructor( 36 | private prisma: PrismaService, 37 | private readonly adaptersService: AdaptersService, 38 | ) { } 39 | 40 | @Post() 41 | create(@Body() adapter: AdapterDTO): Promise { 42 | return this.adaptersService.create(adapter); 43 | } 44 | 45 | @Get() 46 | findAll() { 47 | return this.adaptersService.findAll(); 48 | } 49 | 50 | @Get(':id') 51 | findOne(@Param('id') id: string) { 52 | return this.adaptersService.findOne(id); 53 | } 54 | 55 | @Patch(':id') 56 | update(@Param('id') id: string, @Body() updateAdapterDto: any) { 57 | return this.adaptersService.update(id, updateAdapterDto); 58 | } 59 | 60 | @Delete(':id') 61 | remove(@Param('id') id: string) { 62 | return this.adaptersService.remove(+id); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/secrets/secrets.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { VaultClientProvider } from './secrets.service.provider'; 3 | 4 | @Injectable() 5 | export class SecretsService { 6 | private readonly vaultClient; 7 | 8 | constructor(private readonly vaultClientProvider: VaultClientProvider) { 9 | this.vaultClient = this.vaultClientProvider.getClient(); 10 | } 11 | 12 | async getSecret(path: string, key: string) { 13 | const fullpath = 'kv/'+path; 14 | const kvPairs = await this.vaultClient.read (fullpath); 15 | return kvPairs.__data[key]; 16 | } 17 | 18 | async getSecretByPath(path: string): Promise { 19 | console.log('getSecretByPath method called'); 20 | const fullpath = 'kv/'+path; 21 | const kvPairs = await this.vaultClient.read(fullpath) 22 | return kvPairs.__data; 23 | } 24 | 25 | async setSecret(path: string, value: { [key: string]: string }) { 26 | console.log('SetSecret method called'); 27 | const fullpath = 'kv/'+path; 28 | return await this.vaultClient.write(fullpath, value); 29 | } 30 | 31 | async getAllSecrets(path: string): Promise { 32 | const data: any[] = []; 33 | try { 34 | const fullpath = 'kv/' + path; 35 | const keys = await (await this.vaultClient.list(fullpath)).__data.keys; 36 | for (const key of keys) { 37 | const dataAtKey = await this.getSecretByPath(path + '/' + key); 38 | data.push({ [`${key}`]: dataAtKey }); 39 | } 40 | } catch (e) { 41 | console.log(e); 42 | } 43 | 44 | return data; 45 | } 46 | 47 | async deleteSecret(path: string) { 48 | await this.vaultClient.write('kv/'+ path, {"":""}); 49 | return true; 50 | } 51 | 52 | async deleteAllSecrets(path: any): Promise { 53 | const fullpath = 'kv/' + path; 54 | const keys = await (await this.vaultClient.list(fullpath)).__data.keys; 55 | for (const key of keys) { 56 | await this.deleteSecret(path + '/' + key); 57 | } 58 | return true; 59 | } 60 | } -------------------------------------------------------------------------------- /src/health/health.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HealthController } from './health.controller'; 3 | import { HealthService } from './health.service'; 4 | import { HealthCheckResult } from '@nestjs/terminus'; 5 | 6 | describe.skip('HealthController', () => { 7 | let controller: HealthController; 8 | 9 | const mockHealthService = { 10 | checkHealth: jest.fn().mockResolvedValue({ 11 | status: 'ok', 12 | details: { 13 | 'UCI-API': { 14 | status: 'up', 15 | }, 16 | }, 17 | } as HealthCheckResult), 18 | }; 19 | 20 | beforeEach(async () => { 21 | const module: TestingModule = await Test.createTestingModule({ 22 | controllers: [HealthController], 23 | providers: [HealthService], 24 | }) 25 | .overrideProvider(HealthService) 26 | .useValue(mockHealthService) 27 | .compile(); 28 | 29 | controller = module.get(HealthController); 30 | }); 31 | 32 | describe('getServiceHealth', () => { 33 | it('should return service health check result', async () => { 34 | const mockResult: HealthCheckResult = { 35 | status: 'ok', 36 | details: { 37 | 'UCI-API': { 38 | status: 'up', 39 | }, 40 | }, 41 | }; 42 | 43 | const result = await controller.getServiceHealth(); 44 | expect(result).toEqual(mockResult); 45 | }); 46 | }); 47 | 48 | describe('checkHealth', () => { 49 | it('should return health check result', async () => { 50 | const mockBody: any = {}; 51 | const mockResponse = { 52 | status: 'ok', 53 | details: { 54 | 'UCI-API': { 55 | status: 'up', 56 | }, 57 | }, 58 | }; 59 | const result = await controller.checkHealth(mockBody); 60 | 61 | expect(result).toEqual(mockResponse); 62 | expect(mockHealthService.checkHealth).toBeCalled; 63 | }); 64 | }); 65 | it('should be defined', () => { 66 | expect(controller).toBeDefined(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/interceptors/utils/responseUtils.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | /** 4 | * Helps to create params for error and success response function. 5 | * @param {String} msgId 6 | * @param {String} status 7 | * @param {String} errCode 8 | * @param {String} msg 9 | * @returns {nm$_responseUtil.getParams.params} 10 | */ 11 | function getParams(msgId, status, errCode, msg) { 12 | let params; 13 | params.resmsgid = uuidv4(); 14 | params.msgid = msgId || null; 15 | params.status = status; 16 | params.err = errCode; 17 | params.errmsg = msg; 18 | 19 | return params; 20 | } 21 | 22 | /** 23 | * Creates success response body. 24 | * @param {Object} data 25 | * @returns {nm$_responseUtil.successResponse.response} 26 | */ 27 | export function successResponse(data) { 28 | let response; 29 | response.id = data.apiId; 30 | response.ver = data.apiVersion; 31 | response.ts = new Date(); 32 | response.params = getParams(data.msgid, 'successful', null, null); 33 | response.responseCode = data.responseCode || 'OK'; 34 | response.result = data.result; 35 | 36 | return response; 37 | } 38 | 39 | /** 40 | * Creates error response body. 41 | * @param {Object} data 42 | * @returns {nm$_responseUtil.errorResponse.response} 43 | */ 44 | export function errorResponse(data) { 45 | let response; 46 | response.id = data.apiId; 47 | response.ver = data.apiVersion; 48 | response.ts = new Date(); 49 | response.params = getParams(data.msgId, 'failed', data.errCode, data.errMsg); 50 | response.responseCode = data.responseCode; 51 | response.result = data.result; 52 | return response; 53 | } 54 | 55 | /** 56 | * this function helps to create apiId for error and success response 57 | * @param {String} path 58 | * @returns {getAppIDForRESP.appId|String} 59 | */ 60 | export function getAppIdForResponse(path) { 61 | const arr = path 62 | .split(':')[0] 63 | .split('/') 64 | .filter(function (n) { 65 | return n !== ''; 66 | }); 67 | let appId; 68 | if (arr.length === 1) { 69 | appId = 'api.' + arr[arr.length - 1]; 70 | } else { 71 | appId = 'api.' + arr[arr.length - 2] + '.' + arr[arr.length - 1]; 72 | } 73 | return appId; 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/user-segment/fusionauth/dummyData/generater.ts: -------------------------------------------------------------------------------- 1 | // const organisations = require('./organisations.json'); 2 | // const fs = require('fs'); 3 | 4 | // const baseData = { 5 | // id: '96102c3f-2c22-4614-8dcc-6b130cefe586', 6 | // externalIds: ['96102c3f-2c22-4614-8dcc-6b130cefe586'], 7 | // rootOrgId: 'Bot Owner', 8 | // firstName: 'first name', 9 | // lastName: 'last name', 10 | // userLocation: { 11 | // id: '96102c3f-2c22-4614-8dcc-6b130cefe586', 12 | // state: 'state 1', 13 | // district: 'district 2', 14 | // block: 'block 1', 15 | // cluster: 'district', 16 | // school: 'School 1', 17 | // }, 18 | // roles: 'PUBLIC', 19 | // userType: { 20 | // subType: null, 21 | // type: 'student', 22 | // }, 23 | // customData: {}, 24 | // framework: { 25 | // board: 'State state3', 26 | // gradeLevel: 'Class 1', 27 | // id: 'ap_k-12_1', 28 | // medium: 'English', 29 | // subject: 'English', 30 | // }, 31 | // }; 32 | 33 | // const allStudents = []; 34 | // for (let i = 0; i < organisations.length; i++) { 35 | // let rndInt = Math.floor(Math.random() * 5) + 1; 36 | // let studentData = {}; 37 | 38 | // for (let j = 1; j <= rndInt; j++) { 39 | // studentData = baseData; 40 | // studentData.userLocation = { 41 | // id: organisations[i].id, 42 | // block: organisations[i].block, 43 | // cluster: organisations[i].cluster, 44 | // district: organisations[i].district, 45 | // school: organisations[i].school, 46 | // state: organisations[i].state, 47 | // }; 48 | 49 | // let userNameRandomness = Math.floor(Math.random() * 500) + 1; 50 | 51 | // studentData.firstName = 52 | // organisations[i].school_code + ' firstName-' + userNameRandomness; 53 | // studentData.lastName = 54 | // organisations[i].school_code + ' lastName-' + userNameRandomness; 55 | 56 | // studentData.framework = { 57 | // board: 'State-' + j, 58 | // gradeLevel: 6 - j, 59 | // }; 60 | // studentData.device = { 61 | // type: 'phone', 62 | // deviceID: '96102c3f-2c22-4614-8dcc-6b130cefe586', 63 | // }; 64 | // allStudents.push({ ...studentData }); 65 | // } 66 | // } 67 | 68 | // console.log(allStudents.slice(1, 50)); 69 | 70 | // module.exports = { students: allStudents }; 71 | -------------------------------------------------------------------------------- /src/modules/secrets/secrets.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | NotFoundException, 7 | Param, 8 | Post, 9 | UseInterceptors, 10 | } from '@nestjs/common'; 11 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 12 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 13 | import { AddResponseObjectInterceptor } from '../../interceptors/addResponseObject.interceptor'; 14 | import { AddROToResponseInterceptor } from '../../interceptors/addROToResponse.interceptor'; 15 | import { SecretDTO } from './secret.dto'; 16 | import { SecretsService } from './secrets.service'; 17 | import { getSecretType } from './types'; 18 | 19 | @UseInterceptors( 20 | AddResponseObjectInterceptor, 21 | AddAdminHeaderInterceptor, 22 | AddOwnerInfoInterceptor, 23 | AddROToResponseInterceptor, 24 | ) 25 | @Controller('secret') 26 | export class SecretsController { 27 | constructor(private readonly secretService: SecretsService) {} 28 | 29 | @Post() 30 | async create(@Body() secretDTO: SecretDTO): Promise { 31 | if (secretDTO.type === getSecretType(secretDTO.secretBody)) { 32 | await this.secretService.setSecret( 33 | secretDTO.ownerId + '/' + secretDTO.variableName, 34 | secretDTO.secretBody, 35 | ); 36 | } else { 37 | throw new Error( 38 | 'Type of the secret is not correct ' + 39 | getSecretType(secretDTO.secretBody) + 40 | ' detected', 41 | ); 42 | } 43 | } 44 | 45 | @Get(':variableName') 46 | async findOne( 47 | @Param('variableName') variableName: string, 48 | @Body() body: any, 49 | ): Promise { 50 | try { 51 | if (variableName) { 52 | const data = await this.secretService.getSecretByPath( 53 | body.ownerId + '/' + variableName, 54 | ); 55 | return { [`${variableName}`]: data }; 56 | } else { 57 | return this.secretService.getAllSecrets(body.ownerId); 58 | } 59 | } catch (e) { 60 | return { 61 | status: 404, 62 | }; 63 | } 64 | } 65 | 66 | @Delete(':variableName') 67 | async deleteAll( 68 | @Param('variableName') variableName: string, 69 | @Body() body: any, 70 | ): Promise { 71 | if (variableName) { 72 | return this.secretService.deleteSecret(body.ownerId + '/' + variableName); 73 | } else { 74 | return this.secretService.deleteAllSecrets(body.ownerId); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { AuthGuard, IAuthGuard } from '@nestjs/passport'; 3 | import { User } from './user.entity'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { JwtService } from '@nestjs/jwt'; 6 | import { Reflector } from '@nestjs/core'; 7 | import { IS_PUBLIC_KEY } from './public.decorator'; 8 | 9 | @Injectable() 10 | export class JwtAuthGuard extends AuthGuard('jwt') implements IAuthGuard { 11 | adminToken: string | undefined; 12 | instance: string | undefined; 13 | 14 | constructor( 15 | private readonly configService: ConfigService, 16 | private readonly jwtService: JwtService, 17 | private readonly reflector: Reflector, 18 | ) { 19 | super(); 20 | this.adminToken = configService.get('ADMIN_TOKEN'); 21 | this.instance = configService.get('INSTANCE_ID'); 22 | } 23 | 24 | public handleRequest(err: unknown, user: User): any { 25 | return user; 26 | } 27 | 28 | public async canActivate(context: ExecutionContext): Promise { 29 | const isPublic = this.reflector.getAllAndOverride( 30 | IS_PUBLIC_KEY, 31 | [context.getHandler(), context.getClass()], 32 | ); 33 | if (isPublic) { 34 | return true; 35 | } 36 | await super.canActivate(context); 37 | const request: Request = context.switchToHttp().getRequest(); 38 | let token = ""; 39 | if (request.headers['authorization']) { 40 | token = request.headers['authorization'].split(' ')[1]; 41 | } 42 | let tokenData: any; 43 | const isAdmin = 44 | request.headers['admin-token'] !== undefined && 45 | request.headers['admin-token'] === this.adminToken; 46 | request.headers['isAdmin'] = isAdmin; 47 | console.log(`isAdmin: ${isAdmin}`); 48 | if(isAdmin) return true; //Break circuit early. 49 | try { 50 | let userId; 51 | if(token !== "") { 52 | tokenData = this.jwtService.verify(token); 53 | if (this.instance === 'sunbird') { 54 | userId = tokenData.sub.split(':')[tokenData.sub.split(':').length - 1]; 55 | } else { 56 | userId = tokenData.sub; 57 | } 58 | } 59 | // Either the user is an admin or the user is the owner of the resource 60 | if(userId === undefined) return false; //undefined userId means invalid token 61 | return userId === request.headers['ownerid'] || isAdmin; 62 | } catch (e) { 63 | console.log(e); 64 | return false; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '30 23 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /src/interceptors/addAdminHeader.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { AddAdminHeaderInterceptor } from './addAdminHeader.interceptor'; 2 | import { createMock } from '@golevelup/ts-jest'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ExecutionContext, CallHandler } from '@nestjs/common'; 5 | import { Observable } from 'rxjs'; 6 | 7 | describe('RequestInterceptor', () => { 8 | describe('#intercept correct admin token', () => { 9 | it('t1', async () => { 10 | const executionContext: ExecutionContext = createMock(); 11 | const config: ConfigService = createMock(); 12 | const interceptor = new AddAdminHeaderInterceptor(config); 13 | 14 | const adminToken = 'mocked admin-token'; 15 | const headers = { 16 | 'admin-token': adminToken, 17 | }; 18 | const callHandler: CallHandler = createMock({ 19 | handle: () => 20 | new Observable((observer) => observer.next('next handler')), 21 | }); 22 | ( 23 | executionContext.switchToHttp().getRequest as jest.Mock 24 | ).mockReturnValueOnce({ 25 | body: {}, 26 | headers, 27 | }); 28 | (config.get as jest.Mock).mockReturnValueOnce(adminToken); 29 | await interceptor.intercept(executionContext, callHandler); 30 | expect(callHandler.handle).toBeCalledTimes(1); 31 | expect(executionContext.switchToHttp().getRequest().body).toEqual({ 32 | isAdmin: true, 33 | }); 34 | }); 35 | }); 36 | 37 | describe('#intercept incorrect admin token', () => { 38 | it('t2', async () => { 39 | const executionContext: ExecutionContext = createMock(); 40 | const config: ConfigService = createMock(); 41 | const interceptor = new AddAdminHeaderInterceptor(config); 42 | const adminToken = 'mocked admin-token'; 43 | const headers = { 44 | 'admin-token': adminToken, 45 | }; 46 | const callHandler: CallHandler = createMock({ 47 | handle: () => 48 | new Observable((observer) => observer.next('next handler')), 49 | }); 50 | ( 51 | executionContext.switchToHttp().getRequest as jest.Mock 52 | ).mockReturnValueOnce({ headers, body: {} }); 53 | (config.get as jest.Mock).mockReturnValueOnce( 54 | 'wrong admin token', 55 | ); 56 | expect(executionContext.switchToHttp()).toBeDefined(); 57 | await interceptor.intercept(executionContext, callHandler); 58 | expect(callHandler.handle).toBeCalledTimes(1); 59 | expect(executionContext.switchToHttp().getRequest().body).toEqual({ 60 | isAdmin: false, 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/modules/user-segment/user-segment.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { UserSegment, Service, Prisma } from '../../../prisma/generated/prisma-client-js'; 3 | import { PrismaService } from '../../global-services/prisma.service'; 4 | 5 | @Injectable() 6 | export class UserSegmentService { 7 | logger: Logger; 8 | constructor(private prisma: PrismaService) { 9 | this.logger = new Logger('UserSegmentService'); 10 | } 11 | 12 | async fetchOrCreateExistingService(type: string, config: any, loggerTag?: string): Promise { 13 | // TODO: Check if the service is valid or not. 14 | let service = await this.prisma.service.findFirst({ 15 | where: { 16 | type: type, 17 | config: { 18 | equals: config 19 | } 20 | } 21 | }) 22 | if (service) { 23 | this.logger.log(`${loggerTag} service already created:: ${service.id}`) 24 | } else { 25 | service = await this.prisma.service.create({ 26 | data: { 27 | type: type, 28 | config: config, 29 | } 30 | }) 31 | } 32 | return service; 33 | } 34 | 35 | async create(data: any): Promise { 36 | console.log(data.byPhone?.type); 37 | const allService = data.all?.type !== null && data.all?.type !== undefined ? await this.fetchOrCreateExistingService(data.all.type, data.all.config, "AllService") : null; 38 | const byPhoneService = data.byPhone?.type !== null && data.byPhone?.type !== undefined ? await this.fetchOrCreateExistingService(data.byPhone.type, data.byPhone.config, "ByPhoneService") : null; 39 | const byIDService = data.byId?.type !== null && data.byId?.type !== undefined ? await this.fetchOrCreateExistingService(data.byId.type, data.byId.config, "ByIdService") : null; 40 | 41 | let createData: Prisma.UserSegmentCreateInput = { 42 | name: data.name, 43 | all: allService === null ? undefined : { 44 | connect: { 45 | id: allService.id 46 | } 47 | }, 48 | byPhone: byPhoneService === null ? undefined : { 49 | connect: { 50 | id: byPhoneService.id 51 | } 52 | }, 53 | byID: byIDService === null ? undefined : { 54 | connect: { 55 | id: byIDService.id 56 | } 57 | } 58 | } 59 | return this.prisma.userSegment.create({ data: createData }); 60 | } 61 | 62 | findAll(): Promise { 63 | return this.prisma.userSegment.findMany(); 64 | } 65 | 66 | findOne(id: string): Promise { 67 | return this.prisma.userSegment.findUnique({ where: { id } }); 68 | } 69 | 70 | update(id: string, updateAdapterDto: any) { 71 | return this.prisma.adapter.update({ 72 | where: { 73 | id, 74 | }, 75 | data: updateAdapterDto, 76 | }); 77 | } 78 | 79 | remove(id: string) { 80 | return `This action removes a #${id} adapter`; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FastifyAdapter, 3 | NestFastifyApplication, 4 | } from '@nestjs/platform-fastify'; 5 | import { AppModule } from './app.module'; 6 | import { NestFactory } from '@nestjs/core'; 7 | import helmet from 'fastify-helmet'; 8 | import multipart from 'fastify-multipart'; 9 | import { PrismaService } from './global-services/prisma.service'; 10 | import { MicroserviceOptions, Transport } from '@nestjs/microservices'; 11 | import { 12 | DocumentBuilder, 13 | FastifySwaggerCustomOptions, 14 | SwaggerModule, 15 | } from '@nestjs/swagger'; 16 | import { Logger, ValidationPipe, VersioningType } from '@nestjs/common'; 17 | import { ConfigService } from '@nestjs/config'; 18 | import { join } from 'path'; 19 | import compression from 'fastify-compress'; 20 | 21 | async function bootstrap() { 22 | const logger = new Logger('Main'); 23 | 24 | /** Fastify Application */ 25 | const app = await NestFactory.create( 26 | AppModule, 27 | new FastifyAdapter(), 28 | ); 29 | const configService = app.get(ConfigService); 30 | const brokers = [configService.get('KAFKA_HOST_DEV')]; 31 | 32 | const microservice = app.connectMicroservice({ 33 | transport: Transport.KAFKA, 34 | name: 'SUNBIRD_TELEMETRY', 35 | options: { 36 | client: { 37 | brokers: brokers, 38 | }, 39 | consumer: { 40 | groupId: 'uci-api', 41 | }, 42 | }, 43 | }); 44 | 45 | /** Global prefix: Will result in appending of keyword 'admin' at the start of all the request */ 46 | app.setGlobalPrefix('admin'); 47 | 48 | /** Enable global versioning of all the API's, default version will be v1 */ 49 | // app.enableVersioning({ 50 | // type: VersioningType.URI, 51 | // defaultVersion: '1', 52 | // }); 53 | app.useGlobalPipes(new ValidationPipe()); 54 | 55 | /** OpenApi spec Document builder for Swagger Api Explorer */ 56 | const config = new DocumentBuilder() 57 | .setTitle('UCI') 58 | .setDescription('UCI API description') 59 | .setVersion('1.0') 60 | .build(); 61 | const customOptions: FastifySwaggerCustomOptions = { 62 | uiConfig: { 63 | docExpansion: undefined, 64 | }, 65 | }; 66 | const document = SwaggerModule.createDocument(app, config); 67 | SwaggerModule.setup('api', app, document, customOptions); 68 | 69 | //await app.startAllMicroservices(); 70 | app.register(helmet, { 71 | contentSecurityPolicy: { 72 | directives: { 73 | defaultSrc: [`'self'`], 74 | styleSrc: [`'self'`, `'unsafe-inline'`], 75 | imgSrc: [`'self'`, 'data:', 'validator.swagger.io'], 76 | scriptSrc: [`'self'`, `https: 'unsafe-inline'`], 77 | }, 78 | }, 79 | }); 80 | 81 | app.enableCors(); 82 | await app.register(multipart); 83 | await app.register(compression, { encodings: ['gzip', 'deflate'] }); 84 | app.useStaticAssets({ root: join(__dirname, '../../formUploads') }); 85 | await app.listen(3002, '0.0.0.0'); 86 | 87 | logger.verbose(`APP IS RUNNING ON PORT ${await app.getUrl()}`); 88 | } 89 | bootstrap(); 90 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/conversation-logic.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | ConversationLogic, 4 | Prisma, 5 | } from '../../../prisma/generated/prisma-client-js'; 6 | import { PrismaService } from '../../global-services/prisma.service'; 7 | 8 | @Injectable() 9 | export class ConversationLogicService { 10 | private include = { 11 | transformers: { 12 | include: { 13 | all: true, 14 | }, 15 | }, 16 | }; 17 | constructor(private prisma: PrismaService) {} 18 | 19 | async create(data: any): Promise { 20 | // Loop over TransformerConfig to figure out if we need to create anyone 21 | const tansformerConfigs: any[] = []; 22 | for (let i = 0; i < data.transformers.length; i++) { 23 | try { 24 | const created = await this.prisma.transformerConfig.create({ 25 | data: { 26 | meta: data.transformers[i].meta || {}, 27 | transformer: { 28 | connect: { 29 | id: data.transformers[i].id, 30 | }, 31 | }, 32 | }, 33 | }); 34 | tansformerConfigs.push(created.id); 35 | } catch (e) { 36 | const tc = await this.prisma.transformerConfig.findFirst({ 37 | where: { 38 | meta: { equals: data.transformers[i].meta }, 39 | transformerId: data.transformers[i].id, 40 | }, 41 | }); 42 | console.log('Already exists', tc); 43 | if (tc) { 44 | tansformerConfigs.push(tc.id); 45 | } 46 | } 47 | } 48 | console.info({ tansformerConfigs }); 49 | const createData = { 50 | name: data.name, 51 | description: data.description, 52 | adapter: { 53 | connect: { 54 | id: data.adapter, 55 | }, 56 | }, 57 | transformers: { 58 | connect: tansformerConfigs.map((id) => ({ id })), 59 | }, 60 | }; 61 | return this.prisma.conversationLogic.create({ 62 | data: createData, 63 | }); 64 | } 65 | 66 | findAll(): Promise { 67 | return this.prisma.conversationLogic.findMany({ 68 | include: { 69 | adapter: true, 70 | transformers: true, 71 | }, 72 | }); 73 | } 74 | 75 | findOne(id: string): Promise | null> { 81 | return this.prisma.conversationLogic.findUnique({ 82 | where: { id }, 83 | include: { 84 | adapter: true, 85 | transformers: true, 86 | }, 87 | }); 88 | } 89 | 90 | update(id: string, updateAdapterDto: any) { 91 | return this.prisma.conversationLogic.update({ 92 | where: { 93 | id, 94 | }, 95 | data: updateAdapterDto, 96 | }); 97 | } 98 | 99 | remove(id) { 100 | return `This action removes a #${id} adapter`; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/modules/user-segment/fusionauth/dummyData/ingester.ts: -------------------------------------------------------------------------------- 1 | // const { students } = require('./generateDummyData'); 2 | // const { FusionAuthClient } = require('fusionauth-node-client'); 3 | // const env = require('dotenv').config(); 4 | 5 | // // For staging server 6 | // const fusionAuthURL = process.env.FUSIONAUTH_URL; 7 | // const fusionAuthAPIKey = process.env.FUSIONAUTH_KEY; 8 | // const applicationId = '281c68c4-97f0-4aba-8b2e-cff59fbc038f'; 9 | 10 | // const registerSingleClient = (data, counter) => { 11 | // const client = new FusionAuthClient(fusionAuthAPIKey, fusionAuthURL); 12 | // return client 13 | // .register(undefined, data) 14 | // .then((response) => { 15 | // console.log('User Created', counter); 16 | // }) 17 | // .catch((response) => { 18 | // console.log(JSON.stringify(response)); 19 | // console.log('Failed to register', data.username); 20 | // // console.log(response.errorResponse.fieldErrors); 21 | // return client 22 | // .retrieveUserByLoginId(data.user.username) 23 | // .then((u) => { 24 | // console.log(u.successResponse.user.id); 25 | // return u.successResponse.user.id; 26 | // }) 27 | // .then((id) => { 28 | // console.log('id', id); 29 | // delete data.user.password; 30 | // return client 31 | // .updateUser(id, data) 32 | // .then((r) => { 33 | // console.log('User Updated', counter); 34 | // return r; 35 | // }) 36 | // .catch((r) => { 37 | // console.log('Error in updating user'); 38 | // console.log(util.inspect(r, { showHidden: false, depth: null })); 39 | // return r; 40 | // }); 41 | // }); 42 | // }); 43 | // }; 44 | 45 | // const bulkAdd = async () => { 46 | // const data = students; 47 | // await addOneByOne(data); 48 | // }; 49 | 50 | // async function addOneByOne(data) { 51 | // const requestJSON = data.map((user) => { 52 | // let userRequestJSON = { 53 | // user: { 54 | // username: user.firstName, 55 | // password: 'dummyUser', 56 | // fullName: user.firstName + ' ' + user.lastName, 57 | // active: true, 58 | // data: user, 59 | // }, 60 | // registration: { 61 | // applicationId: applicationId, 62 | // username: user.firstName, 63 | // }, 64 | // }; 65 | // return userRequestJSON; 66 | // }); 67 | // const start = Date.now(); 68 | // const chunkSize = 50; 69 | // let promises = []; 70 | // for (let i = 0; i < requestJSON.length; i++) { 71 | // if (promises.length < chunkSize) { 72 | // console.log('Sending ', i, 'to process'); 73 | // promises.push(registerSingleClient(requestJSON[i], i)); 74 | // } else { 75 | // await Promise.all(promises) 76 | // .then((response) => { 77 | // console.log('Done till', i); 78 | // }) 79 | // .catch((e) => console.log); 80 | // promises = []; 81 | // } 82 | // } 83 | // } 84 | 85 | // bulkAdd(); 86 | -------------------------------------------------------------------------------- /src/modules/form/form.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Post, 6 | Req, 7 | UploadedFiles, 8 | UseInterceptors, 9 | } from '@nestjs/common'; 10 | import { Express } from 'express'; 11 | import { ApiConsumes, ApiTags } from '@nestjs/swagger'; 12 | import { diskStorage } from 'multer'; 13 | import { Request } from 'express'; 14 | import { extname } from 'path'; 15 | import { FormService } from './form.service'; 16 | // eslint-disable-next-line @typescript-eslint/no-var-requires 17 | const fs = require('fs'); 18 | 19 | const editFileName = (req: Request, file: Express.Multer.File, callback) => { 20 | const name = file.originalname.split('.')[0]; 21 | const fileExtName = extname(file.originalname); 22 | const randomName = Array(4) 23 | .fill(null) 24 | .map(() => Math.round(Math.random() * 16).toString(16)) 25 | .join(''); 26 | callback(null, `${name}-${randomName}${fileExtName}`); 27 | }; 28 | 29 | export const formFileFilter = ( 30 | req: Request, 31 | file: Express.Multer.File, 32 | callback, 33 | ) => { 34 | if (!file.originalname.match(/\.(xml|jpg|jpeg|png|gif|mp4|mov|avi|mvi|mkv|flv|webm|pdf)$/)) { 35 | return callback(new BadRequestException('Media format is unsupported!'), false); 36 | } 37 | callback(null, true); 38 | }; 39 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 40 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 41 | import { AddResponseObjectInterceptor } from '../../interceptors/addResponseObject.interceptor'; 42 | import { AddROToResponseInterceptor } from '../../interceptors/addROToResponse.interceptor'; 43 | import { FastifyFileFieldInterceptor } from '../../interceptors/multipleFields.interceptor'; 44 | 45 | @Controller('form') 46 | export class FormController { 47 | constructor(private readonly formService: FormService) {} 48 | 49 | @ApiConsumes('multipart/form-data') 50 | @Post('upload') 51 | @UseInterceptors( 52 | FastifyFileFieldInterceptor([ 53 | {name: 'form', maxCount: 1}, 54 | {name: 'mediaFiles'} 55 | ], { 56 | storage: diskStorage({ 57 | destination: './upload/single', 58 | }), 59 | fileFilter: formFileFilter 60 | }), 61 | AddResponseObjectInterceptor, //sequencing matters here 62 | AddAdminHeaderInterceptor, 63 | AddOwnerInfoInterceptor, 64 | AddROToResponseInterceptor, 65 | ) 66 | async single( 67 | @UploadedFiles() files: {form: Express.Multer.File[], mediaFiles: Express.Multer.File[]}, 68 | ) { 69 | if (!files.form || !files.form[0]) { 70 | throw new BadRequestException('Form file is required!'); 71 | } 72 | const response = await this.formService.uploadForm(files.form[0], files.mediaFiles); 73 | fs.unlink(files.form[0].path, (err) => { 74 | if (err) { 75 | console.log(err); 76 | } 77 | }); 78 | if (files.mediaFiles && files.mediaFiles.length > 0) { 79 | files.mediaFiles.forEach((mediaFile) => { 80 | fs.unlink(mediaFile.path, (err) => { 81 | if (err) { 82 | console.log(err); 83 | } 84 | }); 85 | fs.unlink(`${mediaFile.destination}/${mediaFile.originalname}`, (err) => { 86 | if (err) { 87 | console.log(err); 88 | } 89 | }); 90 | }); 91 | } 92 | return response; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Type, CacheModule } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { PrismaService } from './global-services/prisma.service'; 6 | import { AdaptersModule } from './modules/adapter/adapter.module'; 7 | import { BotModule } from './modules/bot/bot.module'; 8 | import { UserSegmentModule } from './modules/user-segment/user-segment.module'; 9 | import { ConversationLogicModule } from './modules/conversation-logic/conversation-logic.module'; 10 | import { MigrationModule } from './migration/migration.module'; 11 | import { MigrationService } from './migration/migration.service'; 12 | import { SecretsModule } from './modules/secrets/secrets.module'; 13 | import { CommonServiceModule } from './global-services/commonService.module'; 14 | import { SunbirdTelemetryModule } from './sunbird-telemetry/sunbird-telemetry.module'; 15 | import { ServiceModule } from './modules/service/service.module'; 16 | import { TransformerModule } from './modules/transformer/transformer.module'; 17 | import { ServiceService } from './modules/service/service.service'; 18 | import { ServiceController } from './modules/service/service.controller'; 19 | import { AuthModule } from './auth/auth.module'; 20 | import { FormModule } from './modules/form/form.module'; 21 | import { GQLResolverService } from './modules/service/gql.resolver'; 22 | import { SecretsService } from './modules/secrets/secrets.service'; 23 | import { DeviceManagerService } from './modules/user-segment/fusionauth/fusionauth.service'; 24 | import { GetRequestResolverService } from './modules/service/http-get.resolver'; 25 | import { PostRequestResolverService } from './modules/service/http-post.resolver'; 26 | import { HealthModule } from './health/health.module'; 27 | import { FusionAuthClientProvider } from './modules/user-segment/fusionauth/fusionauthClientProvider'; 28 | import { VaultClientProvider } from './modules/secrets/secrets.service.provider'; 29 | import { MonitoringModule } from './monitoring/monitoring.module'; 30 | import { ScheduleModule } from '@nestjs/schedule'; 31 | 32 | import * as redisStore from 'cache-manager-redis-store'; 33 | 34 | @Module({ 35 | imports: [ 36 | ConfigModule.forRoot({ 37 | isGlobal: true, 38 | envFilePath: ['.env.local', '.env'], 39 | }), 40 | AdaptersModule, 41 | BotModule, 42 | MigrationModule, 43 | UserSegmentModule, 44 | ConversationLogicModule, 45 | SecretsModule, 46 | CommonServiceModule, 47 | SunbirdTelemetryModule, 48 | ServiceModule, 49 | TransformerModule, 50 | AuthModule, 51 | FormModule, 52 | HealthModule, 53 | CacheModule.register({ 54 | isGlobal: true, 55 | store: redisStore, 56 | host: process.env.REDIS_HOST, 57 | port: process.env.REDIS_PORT, 58 | ttl: 1800, //seconds 59 | max: 1000 60 | }), 61 | MonitoringModule, 62 | ScheduleModule.forRoot(), 63 | ], 64 | controllers: [AppController, ServiceController], 65 | providers: [ 66 | AppService, 67 | PrismaService, 68 | MigrationService, 69 | ServiceService, 70 | GQLResolverService, 71 | GetRequestResolverService, 72 | PostRequestResolverService, 73 | SecretsService, 74 | FusionAuthClientProvider, 75 | DeviceManagerService, 76 | VaultClientProvider, 77 | ], 78 | }) 79 | export class AppModule {} 80 | -------------------------------------------------------------------------------- /src/migration/migration.service.ts: -------------------------------------------------------------------------------- 1 | import downloadData from './utils/downloadData'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | import { PrismaService } from '../global-services/prisma.service'; 5 | import { ConfigService } from '@nestjs/config'; 6 | 7 | import * as servciesData from './data/getServicesQuery.json'; 8 | import * as transformerData from './data/getTransformerQuery.json'; 9 | import * as adapterData from './data/getAdapterQuery.json'; 10 | import * as userSegmentData from './data/getUserSegmentQuery.json'; 11 | import * as conversationLogicData from './data/getCLQuery.json'; 12 | import * as botData from './data/getBotQuery.json'; 13 | 14 | import { 15 | insertAdapterData, 16 | insertBotData, 17 | insertConversationLogicData, 18 | insertServicesData, 19 | insertTransformerData, 20 | insertUserSegmentData, 21 | } from './utils/insertData'; 22 | 23 | @Injectable() 24 | export class MigrationService { 25 | tableData: any[]; 26 | tableColumns: any; 27 | hasuraURL: string; 28 | hasuraSecret: string; 29 | filePath = './migration/data'; 30 | 31 | constructor( 32 | private prisma: PrismaService, 33 | private configService: ConfigService, 34 | ) { 35 | this.hasuraURL = this.configService.get('GRAPHQL_BASE_URL') || ''; 36 | console.log(this.hasuraURL); 37 | this.hasuraSecret = 38 | this.configService.get('HASURA_GRAPHQL_ADMIN_SECRET') || ''; 39 | this.tableData = []; 40 | this.tableColumns = ['Service Name', 'Total', 'Inserted']; 41 | } 42 | 43 | async downloadData() { 44 | const response = await downloadData( 45 | this.hasuraURL, 46 | this.hasuraSecret, 47 | this.filePath, 48 | ); 49 | if (response) { 50 | console.log('Success'); 51 | } 52 | } 53 | 54 | async deleteAllData() { 55 | await this.prisma.bot.deleteMany({}); 56 | await this.prisma.conversationLogic.deleteMany({}); 57 | await this.prisma.userSegment.deleteMany({}); 58 | await this.prisma.adapter.deleteMany({}); 59 | await this.prisma.transformerConfig.deleteMany({}); 60 | await this.prisma.transformer.deleteMany({}); 61 | await this.prisma.service.deleteMany({}); 62 | } 63 | 64 | async insertData() { 65 | await this.deleteAllData(); 66 | const insertServicesSummary = await insertServicesData( 67 | this.prisma, 68 | servciesData, 69 | ); 70 | this.tableData.push(insertServicesSummary); 71 | const insertTransformerSummary = await insertTransformerData( 72 | this.prisma, 73 | transformerData, 74 | ); 75 | this.tableData.push(insertTransformerSummary); 76 | 77 | const insertAdapterSummary = await insertAdapterData( 78 | this.prisma, 79 | adapterData, 80 | ); 81 | this.tableData.push(insertAdapterSummary); 82 | 83 | const insertUserSegmentSummary = await insertUserSegmentData( 84 | this.prisma, 85 | userSegmentData, 86 | ); 87 | this.tableData.push(insertUserSegmentSummary); 88 | 89 | const insertConversationLogicSummary = await insertConversationLogicData( 90 | this.prisma, 91 | conversationLogicData, 92 | ); 93 | this.tableData.push(insertConversationLogicSummary); 94 | 95 | const insertBotSummary = await insertBotData(this.prisma, botData); 96 | this.tableData.push(insertBotSummary); 97 | console.table(this.tableData); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /prisma/migrations/20220203070953_fix_mtom_relations/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `botId` on the `ConversationLogic` table. All the data in the column will be lost. 5 | - You are about to drop the column `conversationLogicId` on the `Transformer` table. All the data in the column will be lost. 6 | - You are about to drop the `Category` table. If the table is not empty, all the data it contains will be lost. 7 | - You are about to drop the `Post` table. If the table is not empty, all the data it contains will be lost. 8 | - You are about to drop the `_CategoryToPost` table. If the table is not empty, all the data it contains will be lost. 9 | 10 | */ 11 | -- DropForeignKey 12 | ALTER TABLE "ConversationLogic" DROP CONSTRAINT "ConversationLogic_botId_fkey"; 13 | 14 | -- DropForeignKey 15 | ALTER TABLE "Transformer" DROP CONSTRAINT "Transformer_conversationLogicId_fkey"; 16 | 17 | -- DropForeignKey 18 | ALTER TABLE "UserSegment" DROP CONSTRAINT "UserSegment_botId_fkey"; 19 | 20 | -- DropForeignKey 21 | ALTER TABLE "_CategoryToPost" DROP CONSTRAINT "_CategoryToPost_A_fkey"; 22 | 23 | -- DropForeignKey 24 | ALTER TABLE "_CategoryToPost" DROP CONSTRAINT "_CategoryToPost_B_fkey"; 25 | 26 | -- AlterTable 27 | ALTER TABLE "ConversationLogic" DROP COLUMN "botId"; 28 | 29 | -- AlterTable 30 | ALTER TABLE "Transformer" DROP COLUMN "conversationLogicId"; 31 | 32 | -- DropTable 33 | DROP TABLE "Category"; 34 | 35 | -- DropTable 36 | DROP TABLE "Post"; 37 | 38 | -- DropTable 39 | DROP TABLE "_CategoryToPost"; 40 | 41 | -- CreateTable 42 | CREATE TABLE "_ConversationLogicToTransformer" ( 43 | "A" UUID NOT NULL, 44 | "B" UUID NOT NULL 45 | ); 46 | 47 | -- CreateTable 48 | CREATE TABLE "_BotToUserSegment" ( 49 | "A" UUID NOT NULL, 50 | "B" UUID NOT NULL 51 | ); 52 | 53 | -- CreateTable 54 | CREATE TABLE "_BotToConversationLogic" ( 55 | "A" UUID NOT NULL, 56 | "B" UUID NOT NULL 57 | ); 58 | 59 | -- CreateIndex 60 | CREATE UNIQUE INDEX "_ConversationLogicToTransformer_AB_unique" ON "_ConversationLogicToTransformer"("A", "B"); 61 | 62 | -- CreateIndex 63 | CREATE INDEX "_ConversationLogicToTransformer_B_index" ON "_ConversationLogicToTransformer"("B"); 64 | 65 | -- CreateIndex 66 | CREATE UNIQUE INDEX "_BotToUserSegment_AB_unique" ON "_BotToUserSegment"("A", "B"); 67 | 68 | -- CreateIndex 69 | CREATE INDEX "_BotToUserSegment_B_index" ON "_BotToUserSegment"("B"); 70 | 71 | -- CreateIndex 72 | CREATE UNIQUE INDEX "_BotToConversationLogic_AB_unique" ON "_BotToConversationLogic"("A", "B"); 73 | 74 | -- CreateIndex 75 | CREATE INDEX "_BotToConversationLogic_B_index" ON "_BotToConversationLogic"("B"); 76 | 77 | -- AddForeignKey 78 | ALTER TABLE "_ConversationLogicToTransformer" ADD FOREIGN KEY ("A") REFERENCES "ConversationLogic"("id") ON DELETE CASCADE ON UPDATE CASCADE; 79 | 80 | -- AddForeignKey 81 | ALTER TABLE "_ConversationLogicToTransformer" ADD FOREIGN KEY ("B") REFERENCES "Transformer"("id") ON DELETE CASCADE ON UPDATE CASCADE; 82 | 83 | -- AddForeignKey 84 | ALTER TABLE "_BotToUserSegment" ADD FOREIGN KEY ("A") REFERENCES "Bot"("id") ON DELETE CASCADE ON UPDATE CASCADE; 85 | 86 | -- AddForeignKey 87 | ALTER TABLE "_BotToUserSegment" ADD FOREIGN KEY ("B") REFERENCES "UserSegment"("id") ON DELETE CASCADE ON UPDATE CASCADE; 88 | 89 | -- AddForeignKey 90 | ALTER TABLE "_BotToConversationLogic" ADD FOREIGN KEY ("A") REFERENCES "Bot"("id") ON DELETE CASCADE ON UPDATE CASCADE; 91 | 92 | -- AddForeignKey 93 | ALTER TABLE "_BotToConversationLogic" ADD FOREIGN KEY ("B") REFERENCES "ConversationLogic"("id") ON DELETE CASCADE ON UPDATE CASCADE; 94 | -------------------------------------------------------------------------------- /src/migration/utils/downloadData.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import fetch from 'node-fetch'; 3 | import path from 'path'; 4 | 5 | // Hasura based download of all data and saving to JSON files 6 | const getAdapterQuery = `query getAdapterQuery { 7 | adapter { 8 | channel 9 | config 10 | created_at 11 | id 12 | name 13 | provider 14 | updated_at 15 | } 16 | }`; 17 | 18 | const getBotQuery = `query getBotQuery { 19 | bot { 20 | created_at 21 | description 22 | endDate 23 | id 24 | logicIDs 25 | name 26 | ownerID 27 | ownerOrgID 28 | owners 29 | purpose 30 | startDate 31 | startingMessage 32 | status 33 | updated_at 34 | users 35 | } 36 | }`; 37 | 38 | const getCLQuery = `query getCLQuery { 39 | conversationLogic { 40 | adapter 41 | created_at 42 | description 43 | id 44 | name 45 | transformers 46 | updated_at 47 | } 48 | }`; 49 | 50 | const getServicesQuery = `query getServicesQuery { 51 | service { 52 | config 53 | created_at 54 | id 55 | name 56 | type 57 | updated_at 58 | } 59 | }`; 60 | 61 | const getTransformerQuery = `query getTransformerQuery { 62 | transformer { 63 | config 64 | created_at 65 | id 66 | name 67 | service_id 68 | tags 69 | updated_at 70 | } 71 | }`; 72 | 73 | const getUserSegmentQuery = `query getUserSegmentQuery { 74 | userSegment { 75 | all 76 | byID 77 | category 78 | byPhone 79 | count 80 | created_at 81 | description 82 | id 83 | name 84 | } 85 | }`; 86 | 87 | const queries = [ 88 | getAdapterQuery, 89 | getBotQuery, 90 | getCLQuery, 91 | getServicesQuery, 92 | getTransformerQuery, 93 | getUserSegmentQuery, 94 | ]; 95 | 96 | // Download all data from Hasura. 97 | // Save it to a file. 98 | // Return the file path. 99 | export default async function downloadData(hasuraURL, hasuraSecret, filePath) { 100 | const getDataFromHausraAndSaveAsFile = (query) => { 101 | return fetch(hasuraURL, { 102 | method: 'POST', 103 | headers: { 104 | 'Content-Type': 'application/json', 105 | Accept: 'application/json', 106 | 'x-hasura-admin-secret': hasuraSecret, 107 | }, 108 | body: JSON.stringify({ 109 | query, 110 | }), 111 | }) 112 | .then((r) => r.json()) 113 | .then(async (jsonData) => { 114 | try { 115 | // get filepath from query object name 116 | const fp = `${filePath}/${query.split(' ')[1].split('{')[0]}.json`; 117 | const absPath = path.resolve(fp); 118 | // save jsonData to absPath 119 | await fs.writeFileSync(absPath, JSON.stringify(jsonData)); 120 | return true; 121 | } catch (e) { 122 | return false; 123 | } 124 | }) 125 | .catch((e) => { 126 | console.error( 127 | 'CP-downloadData-getDataFromHausraAndSaveAsFile', 128 | e, 129 | e.stack, 130 | ); 131 | return false; 132 | }); 133 | }; 134 | 135 | try { 136 | const data = await Promise.all( 137 | queries.map((query) => getDataFromHausraAndSaveAsFile(query)), 138 | ); 139 | // return true if all elements in data are true 140 | return data.every((e) => e); 141 | } catch (e) { 142 | console.error('CP-downloadData', e, e.stack); 143 | return false; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/migration/data/getTransformerQuery.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "transformer": [ 4 | { 5 | "config": { 6 | "validation": { "in": "xMessage-XML-In", "out": "xMessage-XML-Out" } 7 | }, 8 | "created_at": "2021-06-16T06:03:52.868013+00:00", 9 | "id": "02f010b8-29ce-41e5-be3c-798536a2818b", 10 | "name": "PassThrough", 11 | "service_id": "3fb0e35f-46dc-44cf-95cc-43d1df1c9a11", 12 | "tags": ["generic"], 13 | "updated_at": "2021-06-16T06:03:55.937127+00:00" 14 | }, 15 | { 16 | "config": {}, 17 | "created_at": "2021-06-16T06:03:52.868013+00:00", 18 | "id": "bbf56981-b8c9-40e9-8067-468c2c753659", 19 | "name": "SamagraODKAgg", 20 | "service_id": "94b7c56a-6537-49e3-88e5-4ea548b2f075", 21 | "tags": ["ODK"], 22 | "updated_at": "2021-06-16T06:03:55.937127+00:00" 23 | }, 24 | { 25 | "config": {}, 26 | "created_at": "2021-07-13T07:40:59.266903+00:00", 27 | "id": "7cae4d04-6def-4a9b-8e1e-70dc7fee4e28", 28 | "name": "sunbird-tara-odk", 29 | "service_id": "794b5378-4043-4b48-a086-af8179126cb8", 30 | "tags": ["ODK"], 31 | "updated_at": "2021-07-13T07:40:59.266903+00:00" 32 | }, 33 | { 34 | "config": {}, 35 | "created_at": "2021-11-11T12:33:44.767591+00:00", 36 | "id": "19c6a532-436d-4487-ac98-257343a8cae7", 37 | "name": "RozgarODK", 38 | "service_id": "af9be82e-d59e-4eba-83e4-d19c7f07d8f6", 39 | "tags": ["ODK"], 40 | "updated_at": "2021-11-11T12:33:44.767591+00:00" 41 | }, 42 | { 43 | "config": {}, 44 | "created_at": "2021-11-12T13:14:32.666551+00:00", 45 | "id": "358ee7a7-4e2c-4ea5-acab-92e5d668695c", 46 | "name": "wabot1", 47 | "service_id": "ff170fed-cb7a-463d-a8f9-4c8371241e16", 48 | "tags": ["ODK"], 49 | "updated_at": "2021-11-12T13:14:32.666551+00:00" 50 | }, 51 | { 52 | "config": {}, 53 | "created_at": "2021-11-16T06:29:21.056722+00:00", 54 | "id": "637e2858-18f6-45a3-a492-6c9027963e0c", 55 | "name": "RozgarTesting", 56 | "service_id": "2e38e1bb-1c74-4546-95f3-45a084d0ec3c", 57 | "tags": ["ODK"], 58 | "updated_at": "2021-11-16T06:29:21.056722+00:00" 59 | }, 60 | { 61 | "config": {}, 62 | "created_at": "2021-11-16T12:40:20.506832+00:00", 63 | "id": "d8b69b04-a683-4217-a1bf-9b5f27f0fe7a", 64 | "name": "wabot161121", 65 | "service_id": "74f9f3c0-856b-4299-ab76-ec4a027bf2bf", 66 | "tags": ["ODK"], 67 | "updated_at": "2021-11-16T12:40:20.506832+00:00" 68 | }, 69 | { 70 | "config": {}, 71 | "created_at": "2021-12-16T06:51:03.361755+00:00", 72 | "id": "412e9390-e11e-4732-abb1-1caac476f018", 73 | "name": "LifeSkillsODK", 74 | "service_id": "3a97cd1a-834f-488c-9c39-252261ae23d0", 75 | "tags": ["ODK"], 76 | "updated_at": "2021-12-16T06:51:03.361755+00:00" 77 | }, 78 | { 79 | "config": {}, 80 | "created_at": "2021-12-17T03:55:36.123803+00:00", 81 | "id": "e1d72ae5-fe03-4c9a-83ec-6f689393a558", 82 | "name": "LifeSkills", 83 | "service_id": "7280592a-fd1d-4774-96de-77fd397ade86", 84 | "tags": ["ODK"], 85 | "updated_at": "2021-12-17T03:55:36.123803+00:00" 86 | }, 87 | { 88 | "config": {}, 89 | "created_at": "2022-03-23T11:18:40.429969+00:00", 90 | "id": "774cd134-6657-4688-85f6-6338e2323dde", 91 | "name": "BroadcastTransformer", 92 | "service_id": "94b7c56a-6537-49e3-88e5-4ea548b2f075", 93 | "tags": ["Broadcast"], 94 | "updated_at": "2022-03-23T11:18:40.429969+00:00" 95 | } 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/modules/secrets/secrets.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { SecretsService } from './secrets.service'; 3 | import { VaultClientProvider } from './secrets.service.provider'; 4 | 5 | describe('SecretsService', () => { 6 | let secretsService: SecretsService; 7 | let vaultClientProvider: VaultClientProvider; 8 | let vaultClient: any; 9 | 10 | beforeEach(async () => { 11 | vaultClient = { 12 | read: jest.fn(), 13 | list: jest.fn(), 14 | write: jest.fn(), 15 | clear: jest.fn(), 16 | }; 17 | 18 | const moduleRef = await Test.createTestingModule({ 19 | providers: [ 20 | SecretsService, 21 | { 22 | provide: VaultClientProvider, 23 | useValue: { 24 | getClient: jest.fn(() => vaultClient), 25 | }, 26 | }, 27 | ], 28 | }).compile(); 29 | 30 | secretsService = moduleRef.get(SecretsService); 31 | vaultClientProvider = moduleRef.get(VaultClientProvider); 32 | }); 33 | 34 | describe('getSecret', () => { 35 | it('should return the secret value for the given path and key', async () => { 36 | const path = 'path'; 37 | const key = 'key'; 38 | const secretValue = 'secret'; 39 | 40 | vaultClient.read.mockResolvedValueOnce({ __data: { [key]: secretValue } }); 41 | 42 | const result = await secretsService.getSecret(path, key); 43 | 44 | expect(result).toEqual(secretValue); 45 | expect(vaultClientProvider.getClient).toHaveBeenCalled(); 46 | expect(vaultClient.read).toHaveBeenCalledWith(`kv/${path}`); 47 | }); 48 | }); 49 | 50 | describe('getSecretByPath', () => { 51 | it('should return all secrets for the given path', async () => { 52 | const path = 'path'; 53 | const secrets = { key1: 'secret1', key2: 'secret2' }; 54 | 55 | vaultClient.read.mockResolvedValueOnce({ __data: secrets }); 56 | 57 | const result = await secretsService.getSecretByPath(path); 58 | 59 | expect(result).toEqual(secrets); 60 | expect(vaultClientProvider.getClient).toHaveBeenCalled(); 61 | expect(vaultClient.read).toHaveBeenCalledWith(`kv/${path}`); 62 | }); 63 | }); 64 | 65 | describe('setSecret', () => { 66 | it('should write the given secret value to the specified path', async () => { 67 | const path = 'path'; 68 | const secretValue = { key: 'value' }; 69 | 70 | vaultClient.write.mockResolvedValueOnce(); 71 | 72 | const result = await secretsService.setSecret(path, secretValue); 73 | 74 | expect(result).toBeUndefined(); 75 | expect(vaultClientProvider.getClient).toHaveBeenCalled(); 76 | expect(vaultClient.write).toHaveBeenCalledWith(`kv/${path}`, secretValue); 77 | }); 78 | }); 79 | 80 | describe.skip('getAllSecrets', () => { 81 | it('should return all secrets under the given path', async () => { 82 | const path = 'path'; 83 | const keys = ['key1', 'key2']; 84 | const secrets = { key1: 'secret1', key2: 'secret2' }; 85 | 86 | vaultClient.list.mockResolvedValueOnce({ data: { keys } }); 87 | vaultClient.read.mockImplementation(async (p) => ({ __data: secrets[p.split('/').pop()] })); 88 | 89 | const result = await secretsService.getAllSecrets(path); 90 | 91 | expect(result).toEqual([ 92 | { key1: 'secret1' }, 93 | { key2: 'secret2' }, 94 | ]); 95 | expect(vaultClientProvider.getClient).toHaveBeenCalled(); 96 | expect(vaultClient.list).toHaveBeenCalledWith(path); 97 | expect(vaultClient.read).toHaveBeenCalledTimes(2); 98 | expect(vaultClient.read).toHaveBeenNthCalledWith(1, `kv/${path}/key1`); 99 | expect(vaultClient.read).toHaveBeenCalledWith(`kv/${path}/key2`); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/c4gt.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: C4GT 3 | about: C4GT Community Issues Template 4 | title: "[C4GT] Button for likes" 5 | labels: C4GT Community 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | [Provide a brief description of the feature, including why it is needed and what it will accomplish. You can skip any of Goals, Expected Outcome, Implementation Details, Mockups / Wireframes if they are irrelevant] 12 | 13 | ## Goals 14 | - [ ] [Goal 1] 15 | - [ ] [Goal 2] 16 | - [ ] [Goal 3] 17 | - [ ] [Goal 4] 18 | - [ ] [Goal 5] 19 | 20 | ## Expected Outcome 21 | [Describe in detail what the final product or result should look like and how it should behave.] 22 | 23 | ## Acceptance Criteria 24 | - [ ] [Criteria 1] 25 | - [ ] [Criteria 2] 26 | - [ ] [Criteria 3] 27 | - [ ] [Criteria 4] 28 | - [ ] [Criteria 5] 29 | 30 | ## Implementation Details 31 | [List any technical details about the proposed implementation, including any specific technologies that will be used.] 32 | 33 | ## Mockups / Wireframes 34 | [Include links to any visual aids, mockups, wireframes, or diagrams that help illustrate what the final product should look like. This is not always necessary, but can be very helpful in many cases.] 35 | 36 | --- 37 | 38 | ### Project 39 | [Project Name] 40 | 41 | ### Organization Name: 42 | [Organization Name] 43 | 44 | ### Domain 45 | [Area of governance] 46 | 47 | 82 | 83 | ### Tech Skills Needed: 84 | [Required technical skills for the project] 85 | 86 | ### Mentor(s) 87 | [@Mentor1] [@Mentor2] [@Mentor3] 88 | 89 | ### Complexity 90 | Pick one of [High]/[Medium]/[Low] 91 | 92 | ### Category 93 | Pick one or more of [CI/CD], [Integrations], [Performance Improvement], [Security], [UI/UX/Design], [Bug], [Feature], [Documentation], [Deployment], [Test], [PoC] 94 | 95 | ### Sub Category 96 | Pick one or more of [API], [Database], [Analytics], [Refactoring], [Data Science], [Machine Learning], [Accessibility], [Internationalization], [Localization], [Frontend], [Backend], [Mobile], [SEO], [Configuration], [Deprecation], [Breaking Change], [Maintenance], [Support], [Question], [Technical Debt], [Beginner friendly], [Research], [Reproducible], [Needs Reproduction]. 97 | -------------------------------------------------------------------------------- /src/interceptors/addOwnerInfo.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | 8 | import { MIDDLEWARE as MiddlewareMessages } from '../common/messages'; //done 9 | import { RESPONSE_CODE as ResponseCodes } from '../common/messages'; 10 | import { 11 | catchError, 12 | concat, 13 | from, 14 | ignoreElements, 15 | Observable, 16 | throwError, 17 | } from 'rxjs'; 18 | import { PrismaService } from '../global-services/prisma.service'; 19 | import { Bot, UserSegment } from 'prisma/generated/prisma-client-js'; 20 | 21 | // Nestjs Lifecycle - https://i.stack.imgur.com/2lFhd.jpg 22 | 23 | /** 24 | * @description determine if an array contains one or more items from another array. 25 | * @param {array} haystack the array to search. 26 | * @param {array} arr the array providing items to check for in the haystack. 27 | * @return {boolean} true|false if haystack contains at least one item from arr. 28 | */ 29 | const findOne = (haystack, arr) => { 30 | return arr.some((v) => haystack.includes(v)); 31 | }; 32 | 33 | @Injectable() 34 | export class AddOwnerInfoInterceptor implements NestInterceptor { 35 | constructor(private prisma: PrismaService) {} 36 | 37 | isValidReq(req, urlsWithVerification) { 38 | return ( 39 | findOne(req.originalUrl, urlsWithVerification) && 40 | req.query.id !== undefined && 41 | req.query.id !== null 42 | ); 43 | } 44 | 45 | getReqAsset = (assetString) => { 46 | if (assetString === 'bot') return this.prisma['bot']; 47 | else if (assetString === 'userSegment') return this.prisma['userSegment']; 48 | else if (assetString === 'conversationLogic') 49 | return this.prisma['conversationLogic']; 50 | else if (assetString === 'forms') return 'forms'; 51 | else return null; 52 | }; 53 | 54 | async intercept( 55 | context: ExecutionContext, 56 | next: CallHandler, 57 | ): Promise> { 58 | const req = context.switchToHttp().getRequest(); 59 | const rspObj = req.body.respObj; 60 | const ownerOrgId = req.headers.ownerorgid; 61 | const ownerId = req.headers.ownerid; 62 | const asset = req.body.asset; 63 | 64 | req.body.ownerId = ownerId; 65 | req.body.ownerOrgId = ownerOrgId; 66 | 67 | switch (asset) { 68 | case 'bot': 69 | const assetQueryResponseBot: Bot | null = 70 | await this.prisma.bot.findUnique({ 71 | where: { id: req.params.id }, 72 | }); 73 | if (assetQueryResponseBot?.ownerID === ownerId) { 74 | rspObj.ownerId = ownerId; 75 | } else { 76 | rspObj.errCode = MiddlewareMessages.ADD_OWNER.UNAUTHORIZED_CODE; 77 | rspObj.errMsg = MiddlewareMessages.ADD_OWNER.UNAUTHORIZED_MESSAGE; 78 | rspObj.responseCode = ResponseCodes.CLIENT_ERROR; 79 | } 80 | break; 81 | case 'userSegment': 82 | const assetQueryResponseUS: 83 | | (UserSegment & { 84 | bots: Bot[]; 85 | }) 86 | | null = await this.prisma.userSegment.findUnique({ 87 | where: { id: req.params.id }, 88 | include: { 89 | bots: true, 90 | }, 91 | }); 92 | if ( 93 | assetQueryResponseUS?.bots.map((bot) => bot.ownerID).includes(ownerId) 94 | ) { 95 | rspObj.ownerId = ownerId; 96 | } else { 97 | rspObj.errCode = MiddlewareMessages.ADD_OWNER.UNAUTHORIZED_CODE; 98 | rspObj.errMsg = MiddlewareMessages.ADD_OWNER.UNAUTHORIZED_MESSAGE; 99 | rspObj.responseCode = ResponseCodes.CLIENT_ERROR; 100 | } 101 | break; 102 | case 'conversationLogic': 103 | case 'forms': 104 | case 'secrets': 105 | default: 106 | rspObj.ownerId = ownerId; 107 | return next.handle(); 108 | } 109 | // $ = Observable 110 | const resObj$ = from(rspObj).pipe( 111 | ignoreElements(), 112 | catchError((err) => throwError(err)), 113 | ); 114 | 115 | const main$ = next.handle().pipe((data) => { 116 | rspObj.result = data; 117 | rspObj.endTime = new Date(); 118 | return rspObj; 119 | }); 120 | 121 | return concat(main$, resObj$); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/modules/user-segment/user-segment.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserSegmentService } from './user-segment.service'; 3 | import { PrismaService } from '../../global-services/prisma.service'; 4 | 5 | describe('UserSegmentService', () => { 6 | let service: UserSegmentService; 7 | 8 | const mockUserSegment = { 9 | id: '123', 10 | createdAt: 10000, 11 | updatedAt: 10000, 12 | name: 'mockUserSegment', 13 | all: { 14 | connect: { 15 | id: 7, 16 | }, 17 | }, 18 | byPhone: { 19 | connect: { 20 | id: 8, 21 | }, 22 | }, 23 | byId: { 24 | connect: { 25 | id: 9, 26 | }, 27 | }, 28 | }; 29 | 30 | const mockUserSegmentService = { 31 | userSegment: { 32 | create: jest.fn(async (userSegment) => { 33 | return { 34 | id: '123', 35 | createdAt: 10000, 36 | updatedAt: 10000, 37 | name: userSegment.data.name, 38 | all: { 39 | connect: { 40 | id: 7, 41 | }, 42 | }, 43 | byPhone: { 44 | connect: { 45 | id: 8, 46 | }, 47 | }, 48 | byId: { 49 | connect: { 50 | id: 9, 51 | }, 52 | }, 53 | }; 54 | }), 55 | 56 | findUnique: jest.fn((filter) => { 57 | const resp = { ...mockUserSegment }; 58 | resp.id = filter.where.id; 59 | return resp; 60 | }), 61 | 62 | findMany: jest.fn().mockReturnValue([{ ...mockUserSegment }]), 63 | }, 64 | adapter: { 65 | update: jest.fn((payload) => { 66 | return { 67 | id: payload.where.id, 68 | createdAt: 10000, 69 | updatedAt: 10000, 70 | ...payload.data, 71 | }; 72 | }), 73 | }, 74 | }; 75 | 76 | beforeEach(async () => { 77 | const module: TestingModule = await Test.createTestingModule({ 78 | providers: [ 79 | UserSegmentService, 80 | { 81 | provide: PrismaService, 82 | useValue: mockUserSegmentService, 83 | }, 84 | ], 85 | }).compile(); 86 | 87 | service = module.get(UserSegmentService); 88 | }); 89 | 90 | describe('create', () => { 91 | it('should successfully create a user segment', async () => { 92 | const userSegment = { 93 | name: 'mockUserSegment', 94 | all: 7, 95 | phone: 8, 96 | ID: 9, 97 | }; 98 | expect(await service.create(userSegment)).toEqual({ ...mockUserSegment }); 99 | expect(mockUserSegmentService.userSegment.create).toHaveBeenCalled; 100 | }); 101 | }); 102 | 103 | describe('findOne', () => { 104 | it('should return specific user info', () => { 105 | const resp = { ...mockUserSegment }; 106 | resp.id = '567'; 107 | expect(service.findOne('567')).toEqual({ ...resp }); 108 | expect(mockUserSegmentService.userSegment.findUnique).toHaveBeenCalled; 109 | }); 110 | }); 111 | 112 | describe('findAll', () => { 113 | it('should return array of all user segment object', () => { 114 | expect(service.findAll()).toEqual([{ ...mockUserSegment }]); 115 | expect(mockUserSegmentService.userSegment.findMany).toHaveBeenCalled; 116 | }); 117 | }); 118 | 119 | describe('update', () => { 120 | it('should successfully update user segment', () => { 121 | const resp = { ...mockUserSegment }; 122 | resp.name = 'updatedName'; 123 | expect( 124 | service.update('123', { 125 | name: 'updatedName', 126 | all: { 127 | connect: { 128 | id: 7, 129 | }, 130 | }, 131 | byPhone: { 132 | connect: { 133 | id: 8, 134 | }, 135 | }, 136 | byId: { 137 | connect: { 138 | id: 9, 139 | }, 140 | }, 141 | }), 142 | ).toEqual({ ...resp }); 143 | expect(mockUserSegmentService.adapter.update).toHaveBeenCalled; 144 | }); 145 | }); 146 | 147 | describe('remove', () => { 148 | it('should remove user segment object', () => { 149 | const id = '123'; 150 | expect(service.remove('123')).toEqual( 151 | `This action removes a #${id} adapter`, 152 | ); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uci", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "migrate": "ts-node src/console.ts", 13 | "start": "nest start", 14 | "start:dev": "nest start --watch && yarn run generate-doc", 15 | "doc": "npx compodoc -p tsconfig.doc.json", 16 | "start:debug": "nest start --debug --watch", 17 | "start:prod": "node dist/main", 18 | "start:migrate:prod": "prisma migrate deploy && npm run start:prod", 19 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 20 | "test": "jest", 21 | "test:watch": "jest --watch", 22 | "test:cov": "jest --coverage", 23 | "generate-doc": "npx @compodoc/compodoc -p tsconfig.json -s", 24 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 25 | "test:e2e": "jest --config ./test/jest-e2e.json" 26 | }, 27 | "dependencies": { 28 | "@apollo/client": "^3.6.6", 29 | "@fastify/formbody": "6.0.0", 30 | "@fastify/static": "5.0.0", 31 | "@fusionauth/typescript-client": "^1.38.0", 32 | "@golevelup/ts-jest": "^0.3.3", 33 | "@nestjs/axios": "^2.0.0", 34 | "@nestjs/common": "^8.0.0", 35 | "@nestjs/config": "^1.1.6", 36 | "@nestjs/core": "^8.0.0", 37 | "@nestjs/jwt": "^8.0.0", 38 | "@nestjs/microservices": "^8.4.4", 39 | "@nestjs/passport": "^8.2.1", 40 | "@nestjs/platform-express": "^8.0.0", 41 | "@nestjs/platform-fastify": "^8.2.6", 42 | "@nestjs/schedule": "^4.1.0", 43 | "@nestjs/swagger": "^5.2.0", 44 | "@nestjs/terminus": "^9.2.2", 45 | "@prisma/client": "3", 46 | "@types/multer": "^1.4.7", 47 | "@types/passport-jwt": "^3.0.6", 48 | "add": "^2.0.6", 49 | "apollo-cache-inmemory": "^1.6.6", 50 | "cache-manager": "^4.1.0", 51 | "cache-manager-redis-store": "^2.0.0", 52 | "class-transformer": "^0.5.1", 53 | "class-validator": "^0.13.2", 54 | "cron": "^3.1.7", 55 | "expect-type": "^0.13.0", 56 | "fastify-compress": "3.7.0", 57 | "fastify-helmet": "^7.1.0", 58 | "fastify-multer": "^2.0.2", 59 | "fastify-multipart": "^5.4.0", 60 | "fastify-swagger": "^4.13.1", 61 | "fetch-mock": "^9.11.0", 62 | "graphql": "^16.5.0", 63 | "graphql-tag": "^2.12.6", 64 | "isomorphic-fetch": "^3.0.0", 65 | "jest-mock-extended": "^2.0.5", 66 | "kafkajs": "^1.16.0", 67 | "lodash": "^4.17.21", 68 | "node-vault-client": "^0.6.1", 69 | "p-limit": "3.1.0", 70 | "passport": "^0.5.2", 71 | "passport-jwt": "^4.0.0", 72 | "posthog-node": "^1.3.0", 73 | "prisma": "3", 74 | "prisma-class-generator": "^0.1.12", 75 | "reflect-metadata": "^0.1.13", 76 | "rimraf": "^3.0.2", 77 | "rxjs": "^7.2.0", 78 | "socket.io-client": "^4.7.2", 79 | "swagger-ui-express": "^4.3.0", 80 | "undici": "^5.0.0", 81 | "uuid": "^8.3.2", 82 | "xml2json": "^0.12.0", 83 | "xmlhttprequest": "^1.8.0", 84 | "yarn": "^1.22.18" 85 | }, 86 | "devDependencies": { 87 | "@compodoc/compodoc": "^1.1.18", 88 | "@nestjs/cli": "^8.0.0", 89 | "@nestjs/schematics": "^8.0.0", 90 | "@nestjs/testing": "^8.0.0", 91 | "@types/express": "^4.17.13", 92 | "@types/jest": "^26.0.24", 93 | "@types/node": "^16.11.26", 94 | "@types/supertest": "^2.0.11", 95 | "@typescript-eslint/eslint-plugin": "^4.28.2", 96 | "@typescript-eslint/parser": "^4.28.2", 97 | "@vegardit/prisma-generator-nestjs-dto": "^1.5.0", 98 | "eslint": "^7.30.0", 99 | "eslint-config-prettier": "^8.3.0", 100 | "eslint-plugin-prettier": "^3.4.0", 101 | "jest": "^27.5.1", 102 | "prettier": "^2.3.2", 103 | "supertest": "^6.2.2", 104 | "ts-jest": "^27.0.3", 105 | "ts-loader": "^9.2.3", 106 | "ts-node": "^10.0.0", 107 | "tsconfig-paths": "^3.10.1", 108 | "typescript": "^4.3.5" 109 | }, 110 | "jest": { 111 | "moduleFileExtensions": [ 112 | "js", 113 | "json", 114 | "ts" 115 | ], 116 | "rootDir": "src", 117 | "testRegex": ".*\\.spec\\.ts$", 118 | "transform": { 119 | "^.+\\.(t|j)s$": "ts-jest" 120 | }, 121 | "collectCoverageFrom": [ 122 | "**/*.(t|j)s" 123 | ], 124 | "coverageDirectory": "../coverage", 125 | "testEnvironment": "node" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/modules/user-segment/user-segment.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserSegmentController } from './user-segment.controller'; 3 | import { UserSegmentService } from './user-segment.service'; 4 | 5 | describe('UserSegmentController', () => { 6 | let controller: UserSegmentController; 7 | 8 | const mockUserSegment = { 9 | id: '123', 10 | createdAt: 1000, 11 | updatedAt: 1000, 12 | name: 'mockUserSegment', 13 | all: { 14 | connect: { 15 | id: 7, 16 | }, 17 | }, 18 | byPhone: { 19 | connect: { 20 | id: 8, 21 | }, 22 | }, 23 | byId: { 24 | connect: { 25 | id: 9, 26 | }, 27 | }, 28 | }; 29 | 30 | const mockUserSegmentService = { 31 | create: jest.fn((dto) => { 32 | return { 33 | id: 123, 34 | createdAt: 1000, 35 | updatedAt: 1000, 36 | ...dto, 37 | }; 38 | }), 39 | 40 | findAll: jest.fn(() => { 41 | return [ 42 | { 43 | ...mockUserSegment, 44 | }, 45 | ]; 46 | }), 47 | 48 | findOne: jest.fn((id) => { 49 | const response = { 50 | id, 51 | createdAt: 1000, 52 | updatedAt: 1000, 53 | name: 'mockUserSegment', 54 | all: { 55 | connect: { 56 | id: 7, 57 | }, 58 | }, 59 | byPhone: { 60 | connect: { 61 | id: 8, 62 | }, 63 | }, 64 | byId: { 65 | connect: { 66 | id: 9, 67 | }, 68 | }, 69 | }; 70 | return { ...response }; 71 | }), 72 | 73 | update: jest.fn((id, mockUser) => { 74 | return { 75 | id, 76 | createdAt: 1000, 77 | updatedAt: 1000, 78 | ...mockUser, 79 | }; 80 | }), 81 | 82 | remove: jest.fn((id) => { 83 | return { 84 | id, 85 | }; 86 | }), 87 | }; 88 | 89 | beforeEach(async () => { 90 | const module: TestingModule = await Test.createTestingModule({ 91 | controllers: [UserSegmentController], 92 | providers: [UserSegmentService], 93 | }) 94 | .overrideProvider(UserSegmentService) 95 | .useValue(mockUserSegmentService) 96 | .compile(); 97 | 98 | controller = module.get(UserSegmentController); 99 | }); 100 | 101 | describe('create', () => { 102 | it('should create a user object', () => { 103 | const user: any = {}; 104 | 105 | expect(controller.create(user)).toEqual({ 106 | id: expect.any(Number), 107 | createdAt: expect.any(Number), 108 | updatedAt: expect.any(Number), 109 | ...user, 110 | }); 111 | expect(mockUserSegmentService.create).toBeCalled; 112 | }); 113 | }); 114 | 115 | describe('findAll', () => { 116 | it('should return the array of mockUser object', () => { 117 | expect(controller.findAll()).toEqual([ 118 | { 119 | ...mockUserSegment, 120 | }, 121 | ]); 122 | expect(mockUserSegmentService.findAll()).toBeCalled; 123 | }); 124 | }); 125 | 126 | describe('findOne', () => { 127 | it('should return specific user info', () => { 128 | const response = { ...mockUserSegment }; 129 | response.id = '456'; 130 | 131 | expect(controller.findOne('456')).toEqual({ ...response }); 132 | expect(mockUserSegmentService.findOne('456')).toBeCalled; 133 | }); 134 | }); 135 | 136 | describe('update', () => { 137 | it('should return the updated user object', () => { 138 | const response = { ...mockUserSegment }; 139 | response.name = 'mock2'; 140 | 141 | expect( 142 | controller.update('123', { 143 | name: 'mock2', 144 | all: { 145 | connect: { 146 | id: 7, 147 | }, 148 | }, 149 | byPhone: { 150 | connect: { 151 | id: 8, 152 | }, 153 | }, 154 | byId: { 155 | connect: { 156 | id: 9, 157 | }, 158 | }, 159 | }), 160 | ).toEqual({ ...response }); 161 | 162 | expect(mockUserSegmentService.update).toBeCalled; 163 | }); 164 | }); 165 | 166 | describe('delete', () => { 167 | it('should successfuly delete a user segment', () => { 168 | const id = '123'; 169 | expect(controller.remove(id)).toEqual({ id }); 170 | 171 | expect(mockUserSegmentService.remove).toBeCalled; 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/modules/adapter/adapter.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdaptersService } from './adapter.service'; 3 | import { PrismaService } from '../../global-services/prisma.service'; 4 | import { createMock } from '@golevelup/ts-jest'; 5 | import { TelemetryService } from '../../global-services/telemetry.service'; 6 | 7 | describe('AdaptersService', () => { 8 | let service: AdaptersService; 9 | let mockPrismaService: PrismaService; 10 | 11 | const mockAdapter = { 12 | id: 'mockID', 13 | createdAt: 1000000000000, 14 | updatedAt: 1000000000000, 15 | channel: 'mockChannel', 16 | provider: 'mockProvider', 17 | config: { 18 | phone: '0000000000', 19 | HSM_ID: 'mockHSMID', 20 | '2WAY': 'mock2WAY', 21 | credentials: { ['mockKey']: 'mockKeys' }, 22 | }, 23 | name: 'mockName', 24 | }; 25 | 26 | const mockPrismaServiceValue = { 27 | adapter: { 28 | create: jest.fn(async (adapterDTO) => { 29 | return { 30 | id: 'mockID', 31 | createdAt: 1000000000000, 32 | updatedAt: 1000000000000, 33 | channel: adapterDTO.data.channel, 34 | provider: adapterDTO.data.provider, 35 | config: adapterDTO.data.config, 36 | name: adapterDTO.data.name, 37 | }; 38 | }), 39 | findMany: jest.fn().mockReturnValue([ 40 | { 41 | ...mockAdapter, 42 | }, 43 | ]), 44 | findUnique: jest.fn((filter) => { 45 | const res = { ...mockAdapter }; 46 | res.id = filter.where.id; 47 | return res; 48 | }), 49 | update: jest.fn((payload) => { 50 | return { 51 | id: payload.where.id, 52 | createdAt: 1000000000000, 53 | updatedAt: 1000000000000, 54 | ...payload.data, 55 | }; 56 | }), 57 | }, 58 | }; 59 | 60 | beforeEach(async () => { 61 | const module: TestingModule = await Test.createTestingModule({ 62 | providers: [ 63 | { 64 | provide: PrismaService, 65 | useValue: { ...mockPrismaServiceValue }, 66 | }, 67 | AdaptersService, 68 | TelemetryService, 69 | ], 70 | }) 71 | .overrideProvider(TelemetryService) 72 | .useValue(createMock()) 73 | .compile(); 74 | 75 | service = module.get(AdaptersService); 76 | mockPrismaService = module.get(PrismaService); 77 | }); 78 | 79 | it('Service should be defined', () => { 80 | expect(service).toBeDefined(); 81 | }); 82 | 83 | describe('root', () => { 84 | it('Create() | Should return "Adapter" object', async () => { 85 | expect( 86 | await service.create({ 87 | channel: 'mockChannel', 88 | provider: 'mockProvider', 89 | name: 'mockName', 90 | config: { 91 | phone: '0000000000', 92 | HSM_ID: 'mockHSMID', 93 | '2WAY': 'mock2WAY', 94 | credentials: { ['mockKey']: 'mockKeys' }, 95 | }, 96 | }), 97 | ).toEqual({ 98 | ...mockAdapter, 99 | }); 100 | 101 | expect(mockPrismaService.adapter.create).toHaveBeenCalled(); 102 | }); 103 | 104 | it('findAll() | Should return array of "Adapter" objects', () => { 105 | expect(service.findAll()).toEqual([ 106 | { 107 | ...mockAdapter, 108 | }, 109 | ]); 110 | 111 | expect(mockPrismaService.adapter.findMany).toHaveBeenCalled(); 112 | }); 113 | 114 | it('findOne() | Should return "Adapter" object', () => { 115 | const res = { ...mockAdapter }; 116 | res.id = 'mockID2'; 117 | expect(service.findOne('mockID2')).toEqual({ ...res }); 118 | 119 | expect(mockPrismaService.adapter.findUnique).toHaveBeenCalled(); 120 | }); 121 | 122 | it('update() | Should return "Adapter" object', () => { 123 | const res = { ...mockAdapter }; 124 | res.name = 'mockName2'; 125 | expect( 126 | service.update('mockID', { 127 | channel: 'mockChannel', 128 | provider: 'mockProvider', 129 | name: 'mockName2', 130 | config: { 131 | phone: '0000000000', 132 | HSM_ID: 'mockHSMID', 133 | '2WAY': 'mock2WAY', 134 | credentials: { ['mockKey']: 'mockKeys' }, 135 | }, 136 | }), 137 | ).toEqual({ 138 | ...res, 139 | }); 140 | 141 | expect(mockPrismaService.adapter.update).toHaveBeenCalled(); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/modules/transformer/transformer.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { TransformerService } from './transformer.service'; 2 | import { TransformerController } from './transformer.controller'; 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 5 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 6 | import { createMock } from '@golevelup/ts-jest'; 7 | 8 | describe('TransformerController', () => { 9 | let controller: TransformerController; 10 | 11 | const mockTransformerData = { 12 | id: '123', 13 | createdAt: 1000, 14 | updatedAt: 1000, 15 | name: 'mockTransformerService', 16 | service: { create: 'create' }, 17 | tags: ['tag1', 'tag2'], 18 | config: { 19 | value: '123', 20 | }, 21 | }; 22 | 23 | const mockTransformerService = { 24 | create: jest.fn((data) => { 25 | return { 26 | id: '123', 27 | createdAt: 1000, 28 | updatedAt: 1000, 29 | name: data.name, 30 | service: { create: data.service }, 31 | tags: data.tags, 32 | config: data.config, 33 | }; 34 | }), 35 | 36 | findAll: jest.fn(() => { 37 | return [ 38 | { 39 | ...mockTransformerData, 40 | }, 41 | ]; 42 | }), 43 | 44 | findOne: jest.fn((id) => { 45 | const res = { 46 | id, 47 | createdAt: 1000, 48 | updatedAt: 1000, 49 | name: 'mockTransformerService', 50 | service: { create: 'create' }, 51 | tags: ['tag1', 'tag2'], 52 | config: { 53 | value: '123', 54 | }, 55 | }; 56 | return { ...res }; 57 | }), 58 | 59 | update: jest.fn((id, data) => { 60 | return { 61 | id, 62 | createdAt: 1000, 63 | updatedAt: 1000, 64 | name: data.name, 65 | service: { create: data.service }, 66 | tags: data.tags, 67 | config: data.config, 68 | }; 69 | }), 70 | 71 | remove: jest.fn((id) => { 72 | return { 73 | id, 74 | }; 75 | }), 76 | }; 77 | 78 | beforeEach(async () => { 79 | const module: TestingModule = await Test.createTestingModule({ 80 | controllers: [TransformerController], 81 | providers: [TransformerService], 82 | }) 83 | .overrideProvider(TransformerService) 84 | .useValue(mockTransformerService) 85 | .overrideInterceptor(AddAdminHeaderInterceptor) 86 | .useValue(createMock()) 87 | .overrideInterceptor(AddOwnerInfoInterceptor) 88 | .useValue(createMock()) 89 | .compile(); 90 | 91 | controller = module.get(TransformerController); 92 | }); 93 | 94 | describe('create', () => { 95 | it('should create a new transformer object', () => { 96 | const data = { 97 | name: 'mockTransformerService', 98 | service: 'create', 99 | tags: ['tag1', 'tag2'], 100 | config: { 101 | value: '123', 102 | }, 103 | }; 104 | expect(controller.create(data)).toEqual(mockTransformerData); 105 | expect(mockTransformerService.create).toBeCalled; 106 | }); 107 | }); 108 | 109 | describe('findAll', () => { 110 | it('should return the array of transformer objects', () => { 111 | expect(controller.findAll()).toEqual([{ ...mockTransformerData }]); 112 | expect(controller.findAll).toBeCalled; 113 | }); 114 | }); 115 | 116 | describe('findOne', () => { 117 | it('should return the specific transformer object', () => { 118 | expect(controller.findOne('123')).toEqual({ ...mockTransformerData }); 119 | expect(mockTransformerService.findOne).toBeCalled; 120 | }); 121 | }); 122 | 123 | describe('update', () => { 124 | it('should return the updated transformer object', () => { 125 | const mockResponse = { ...mockTransformerData }; 126 | mockResponse.name = 'updatedName'; 127 | expect( 128 | controller.update('123', { 129 | name: 'updatedName', 130 | service: 'create', 131 | tags: ['tag1', 'tag2'], 132 | config: { 133 | value: '123', 134 | }, 135 | }), 136 | ).toEqual(mockResponse); 137 | expect(mockTransformerService.update).toBeCalled; 138 | }); 139 | }); 140 | 141 | describe('remove', () => { 142 | it('should successfully delete a transformer object', () => { 143 | const id = '123'; 144 | expect(controller.remove('123')).toEqual({ id }); 145 | expect(mockTransformerService.remove).toBeCalled; 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /src/modules/service/gql.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { ConfigService } from '@nestjs/config'; 4 | import { GQLResolverService } from './gql.resolver'; 5 | import { SecretsService } from '../secrets/secrets.service'; 6 | import { TelemetryService } from '../../global-services/telemetry.service'; 7 | import { createMock } from '@golevelup/ts-jest'; 8 | import { User } from './schema/user.dto'; 9 | import { ServiceQueryType } from './enum'; 10 | 11 | describe.skip('SecretsService', () => { 12 | let service: GQLResolverService; 13 | let telemetryService: TelemetryService; 14 | 15 | beforeEach(async () => { 16 | const module: TestingModule = await Test.createTestingModule({ 17 | providers: [ 18 | GQLResolverService, 19 | SecretsService, 20 | ConfigService, 21 | { 22 | provide: TelemetryService, 23 | useValue: { 24 | client: { 25 | capture: jest.fn((data: any) => { 26 | // console.log('capture called', data); 27 | }), 28 | }, 29 | }, 30 | }, 31 | ], 32 | }).compile(); 33 | 34 | service = module.get(GQLResolverService); 35 | telemetryService = module.get(TelemetryService); 36 | }); 37 | 38 | it('should be defined', () => { 39 | expect(service).toBeDefined(); 40 | }); 41 | 42 | it('should create a valid client even with wrong inputs', () => { 43 | const client = service.getClient('test', {}); 44 | expect(client.link).toBeDefined(); 45 | }); 46 | 47 | it('should notify correctly', async () => { 48 | // Connected to a Pipedream workflow with the following configuration: 49 | /* 50 | https://pipedream.com/@chakshugautam/uci-errorwebhook-workflow-p_3nCeZaq/inspect 51 | export default defineComponent({ 52 | async run({ steps, $ }) { 53 | await $.respond({ 54 | status: 200, 55 | headers: {}, 56 | body: { 57 | "status": "Recieved", 58 | "user": steps.trigger.event.body?.user, 59 | }, 60 | }) 61 | }}, 62 | }) 63 | */ 64 | const user: User = { 65 | id: 'testUserId', 66 | externalIds: ['test'], 67 | rootOrgId: '', 68 | }; 69 | const response = await service.notifyOnError( 70 | 'https://eo3kiu96phwev11.m.pipedream.net', 71 | user, 72 | 'test Error', 73 | ); 74 | expect(response).toBeDefined(); 75 | expect(response.user).toEqual(user); 76 | }); 77 | 78 | it('should not notify - send telemetry event', async () => { 79 | const user: User = { 80 | id: 'testUserId', 81 | externalIds: ['test'], 82 | rootOrgId: '', 83 | }; 84 | const response = await service.notifyOnError( 85 | 'https://wrong-url.com', 86 | user, 87 | 'test Error', 88 | ); 89 | expect(response).toBeDefined(); 90 | expect(response.error).toBeDefined(); 91 | expect(telemetryService.client.capture).toBeCalledTimes(1); 92 | }); 93 | 94 | // <-------------------- This test case needs to be fixed --------------------> 95 | 96 | // it('should get users', async () => { 97 | // const gqlConfig = { 98 | // url: 'https://api.pipedream.com/users', 99 | // query: 100 | // "query Query {users: getUsersByQuery(queryString: \"(((data.userLocation.state : 'Haryana') AND (data.userLocation.district : 'Ambala')) OR ((data.userLocation.state : 'Haryana') AND (data.userLocation.district : 'Panipat') AND (data.userLocation.block : 'Panipat'))) AND (data.roles : PUBLIC) AND (data.userType.type : student)\") {lastName firstName device customData externalIds framework lastName roles rootOrgId userLocation userType}}", 101 | // cadence: { 102 | // perPage: 10000, 103 | // retries: 5, 104 | // timeout: 60, 105 | // concurrent: true, 106 | // pagination: false, 107 | // 'retries-interval': 10, 108 | // }, 109 | // pageParam: 'page', 110 | // credentials: { 111 | // vault: 'samagra', 112 | // variable: 'dummygql', 113 | // }, 114 | // verificationParams: { 115 | // phone: '9415787824', 116 | // }, 117 | // errorNotificationWebhook: 'https://eo3kiu96phwev11.m.pipedream.net', 118 | // }; 119 | // const users = await service.resolve( 120 | // ServiceQueryType.byPhone, 121 | // gqlConfig, 122 | // 'test', 123 | // undefined 124 | // ); 125 | // expect(users).toBeInstanceOf(Array); 126 | // }); 127 | 128 | afterAll(async () => { 129 | return true; 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/modules/adapter/adapter.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdaptersController } from './adapter.controller'; 3 | import { AdaptersService } from './adapter.service'; 4 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 5 | import { createMock } from '@golevelup/ts-jest'; 6 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 7 | import { PrismaService } from '../../global-services/prisma.service'; 8 | 9 | describe('AdaptersController', () => { 10 | let controller: AdaptersController; 11 | let service: AdaptersService; 12 | 13 | const mockAdapter = { 14 | id: 'mockID', 15 | createdAt: 1000000000000, 16 | updatedAt: 1000000000000, 17 | channel: 'mockChannel', 18 | provider: 'mockProvider', 19 | config: { 20 | phone: '0000000000', 21 | HSM_ID: 'mockHSMID', 22 | '2WAY': 'mock2WAY', 23 | credentials: { ['mockKey']: 'mockKeys' }, 24 | }, 25 | name: 'mockName', 26 | }; 27 | 28 | beforeEach(async () => { 29 | const module: TestingModule = await Test.createTestingModule({ 30 | controllers: [AdaptersController], 31 | providers: [ 32 | { 33 | provide: AdaptersService, 34 | useValue: { 35 | create: jest.fn((adapterDTO) => { 36 | return { 37 | id: 'mockID', 38 | createdAt: 1000000000000, 39 | updatedAt: 1000000000000, 40 | ...adapterDTO, 41 | }; 42 | }), 43 | findAll: jest.fn().mockReturnValue([ 44 | { 45 | ...mockAdapter, 46 | }, 47 | ]), 48 | findOne: jest.fn((id) => { 49 | const res = { ...mockAdapter }; 50 | res.id = id; 51 | return res; 52 | }), 53 | update: jest.fn((ID, adapterDTO) => { 54 | return { 55 | id: ID, 56 | createdAt: 1000000000000, 57 | updatedAt: 1000000000000, 58 | ...adapterDTO, 59 | }; 60 | }), 61 | }, 62 | }, 63 | PrismaService, 64 | ], 65 | }) 66 | .overrideProvider(PrismaService) 67 | .useValue(createMock()) 68 | .overrideInterceptor(AddAdminHeaderInterceptor) 69 | .useValue(createMock()) 70 | .overrideInterceptor(AddOwnerInfoInterceptor) 71 | .useValue(createMock()) 72 | .compile(); 73 | 74 | controller = module.get(AdaptersController); 75 | service = module.get(AdaptersService); 76 | }); 77 | 78 | it('Controller should be defined', () => { 79 | expect(controller).toBeDefined(); 80 | }); 81 | 82 | describe('root', () => { 83 | it('Create() | Should return "Adapter" object', () => { 84 | expect( 85 | controller.create({ 86 | channel: 'mockChannel', 87 | provider: 'mockProvider', 88 | name: 'mockName', 89 | config: { 90 | phone: '0000000000', 91 | HSM_ID: 'mockHSMID', 92 | '2WAY': 'mock2WAY', 93 | credentials: { ['mockKey']: 'mockKeys' }, 94 | }, 95 | }), 96 | ).toEqual({ 97 | ...mockAdapter, 98 | }); 99 | 100 | expect(service.create).toHaveBeenCalled(); 101 | }); 102 | 103 | it('findAll() | Should return array of "Adapter" objects', () => { 104 | expect(controller.findAll()).toEqual([ 105 | { 106 | ...mockAdapter, 107 | }, 108 | ]); 109 | 110 | expect(service.findAll).toHaveBeenCalled(); 111 | }); 112 | 113 | it('findOne() | Should return "Adapter" object', () => { 114 | const res = { ...mockAdapter }; 115 | res.id = 'mockID2'; 116 | expect(controller.findOne('mockID2')).toEqual({ ...res }); 117 | 118 | expect(service.findOne).toHaveBeenCalled(); 119 | }); 120 | 121 | it('update() | Should return "Adapter" object', () => { 122 | const res = { ...mockAdapter }; 123 | res.name = 'mockName2'; 124 | expect( 125 | controller.update('mockID', { 126 | channel: 'mockChannel', 127 | provider: 'mockProvider', 128 | name: 'mockName2', 129 | config: { 130 | phone: '0000000000', 131 | HSM_ID: 'mockHSMID', 132 | '2WAY': 'mock2WAY', 133 | credentials: { ['mockKey']: 'mockKeys' }, 134 | }, 135 | }), 136 | ).toEqual({ 137 | ...res, 138 | }); 139 | 140 | expect(service.update).toHaveBeenCalled(); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = env("PSQL_DB_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | output = "./generated/prisma-client-js" 12 | } 13 | 14 | enum BotStatus { 15 | ENABLED 16 | DISABLED 17 | DRAFT 18 | PINNED 19 | } 20 | 21 | model Adapter { 22 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | 26 | channel String 27 | provider String 28 | config Json 29 | name String? 30 | 31 | ConversationLogic ConversationLogic[] 32 | } 33 | 34 | model Board { 35 | id Int @id @default(autoincrement()) 36 | name String 37 | } 38 | 39 | model Service { 40 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 41 | createdAt DateTime @default(now()) 42 | updatedAt DateTime @updatedAt 43 | 44 | type String 45 | config Json? 46 | name String? 47 | Transformer Transformer[] 48 | UserSegmentByID UserSegment[] @relation("UserSegmentByID") 49 | UserSegmentByPhone UserSegment[] @relation("UserSegmentByPhone") 50 | UserSegmentAll UserSegment[] @relation("UserSegmentAll") 51 | } 52 | 53 | model Transformer { 54 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 55 | createdAt DateTime @default(now()) 56 | updatedAt DateTime @updatedAt 57 | 58 | name String 59 | tags String[] 60 | config Json 61 | service Service @relation(fields: [serviceId], references: [id]) 62 | serviceId String @db.Uuid 63 | TranformerConfig TransformerConfig[] 64 | } 65 | 66 | model TransformerConfig { 67 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 68 | createdAt DateTime @default(now()) 69 | updatedAt DateTime @updatedAt 70 | meta Json 71 | 72 | transformer Transformer @relation(fields: [transformerId], references: [id]) 73 | transformerId String @db.Uuid 74 | 75 | ConversationLogic ConversationLogic? @relation(fields: [conversationLogicId], references: [id]) 76 | conversationLogicId String? @db.Uuid 77 | } 78 | 79 | model Bot { 80 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 81 | createdAt DateTime @default(now()) 82 | updatedAt DateTime @updatedAt 83 | 84 | name String @unique 85 | startingMessage String 86 | users UserSegment[] 87 | logicIDs ConversationLogic[] 88 | ownerID String? 89 | ownerOrgID String? 90 | purpose String? 91 | description String? 92 | startDate DateTime? 93 | endDate DateTime? 94 | status BotStatus @default(DRAFT) 95 | tags String[] 96 | botImage String? 97 | meta Json? 98 | schedules Schedules[] 99 | } 100 | 101 | model UserSegment { 102 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 103 | createdAt DateTime @default(now()) 104 | updatedAt DateTime @updatedAt 105 | 106 | name String @unique 107 | description String? 108 | count Int @default(0) 109 | category String? 110 | 111 | all Service? @relation(fields: [allServiceID], references: [id], name: "UserSegmentAll") 112 | byID Service? @relation(fields: [byIDServiceID], references: [id], name: "UserSegmentByID") 113 | byPhone Service? @relation(fields: [byPhoneServiceID], references: [id], name: "UserSegmentByPhone") 114 | 115 | allServiceID String? @db.Uuid 116 | byPhoneServiceID String? @db.Uuid 117 | byIDServiceID String? @db.Uuid 118 | 119 | bots Bot[] 120 | botId String? @db.Uuid 121 | } 122 | 123 | model ConversationLogic { 124 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 125 | name String 126 | createdAt DateTime @default(now()) 127 | updatedAt DateTime @updatedAt 128 | 129 | description String? 130 | 131 | adapter Adapter @relation(fields: [adapterId], references: [id]) 132 | adapterId String @db.Uuid 133 | bots Bot[] 134 | transformers TransformerConfig[] 135 | } 136 | 137 | model Schedules { 138 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid() 139 | name String @default("") 140 | createdAt DateTime @default(now()) 141 | scheduledAt DateTime 142 | authToken String 143 | bot Bot @relation(fields: [botId], references: [id]) 144 | config Json 145 | botId String @db.Uuid 146 | } 147 | -------------------------------------------------------------------------------- /src/modules/conversation-logic/conversation-logic.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConversationLogicController } from './conversation-logic.controller'; 3 | import { ConversationLogicService } from './conversation-logic.service'; 4 | import { AddAdminHeaderInterceptor } from '../../interceptors/addAdminHeader.interceptor'; 5 | import { createMock } from '@golevelup/ts-jest'; 6 | import { AddOwnerInfoInterceptor } from '../../interceptors/addOwnerInfo.interceptor'; 7 | 8 | describe.skip('ConversationLogicController', () => { 9 | let controller: ConversationLogicController; 10 | let service: ConversationLogicService; 11 | class mockConversationLogic { 12 | public id: string; 13 | public name: string; 14 | public description: string | null; 15 | public adapterId: string; 16 | 17 | constructor(...args) { 18 | if (args[0]) { 19 | this.id = args[0].id ? args[0].id : 'mockId'; 20 | this.name = args[0].name ? args[0].name : 'mockName'; 21 | this.description = args[0].description 22 | ? args[0].description 23 | : 'mockDescription'; 24 | this.adapterId = args[0].adapter ? args[0].adapter : 'mockAdapterId'; 25 | } 26 | } 27 | } 28 | 29 | const mockConversationLogicServiceValue = { 30 | create: jest.fn((mockCreateConversationLogicDto) => { 31 | const res = new mockConversationLogic(mockCreateConversationLogicDto); 32 | return { ...res }; 33 | }), 34 | findAll: jest.fn().mockReturnValue([ 35 | { 36 | ...new mockConversationLogic(), 37 | }, 38 | ]), 39 | findOne: jest.fn((id) => { 40 | const res = new mockConversationLogic({}); 41 | res.id = id; 42 | return { ...res }; 43 | }), 44 | update: jest.fn((id, mockUpdateConversationLogicDto) => { 45 | const res = new mockConversationLogic(mockUpdateConversationLogicDto); 46 | res.id = id; 47 | return { ...res }; 48 | }), 49 | remove: jest.fn((id) => { 50 | const res = new mockConversationLogic({}); 51 | res.id = id; 52 | return { ...res }; 53 | }), 54 | }; 55 | 56 | beforeEach(async () => { 57 | const module: TestingModule = await Test.createTestingModule({ 58 | controllers: [ConversationLogicController], 59 | providers: [ 60 | { 61 | provide: ConversationLogicService, 62 | useValue: { ...mockConversationLogicServiceValue }, 63 | }, 64 | ], 65 | }) 66 | .overrideInterceptor(AddAdminHeaderInterceptor) 67 | .useValue(createMock()) 68 | .overrideInterceptor(AddOwnerInfoInterceptor) 69 | .useValue(createMock()) 70 | .compile(); 71 | 72 | controller = module.get( 73 | ConversationLogicController, 74 | ); 75 | service = module.get(ConversationLogicService); 76 | }); 77 | 78 | it('Controller should be defined', () => { 79 | expect(controller).toBeDefined(); 80 | }); 81 | 82 | describe('root', () => { 83 | it('Create() | Should return "ConversationLogic" object', () => { 84 | const res = new mockConversationLogic({ 85 | id: 'mockId2', 86 | name: 'mockName2', 87 | description: 'mockDescription2', 88 | adapter: 'mockAdapter', 89 | }); 90 | 91 | expect( 92 | controller.create({ 93 | data: { 94 | id: 'mockId2', 95 | name: 'mockName2', 96 | description: 'mockDescription2', 97 | adapter: 'mockAdapter', 98 | }, 99 | }), 100 | ).toEqual({ ...res }); 101 | 102 | expect(service.create).toHaveBeenCalled(); 103 | }); 104 | 105 | it('findAll() | Should return array of "ConversationLogic" objects', () => { 106 | expect(controller.findAll()).toEqual([ 107 | { ...new mockConversationLogic() }, 108 | ]); 109 | 110 | expect(service.findAll).toHaveBeenCalled(); 111 | }); 112 | 113 | it('findOne() | Should return "ConversationLogic" object', () => { 114 | const res = new mockConversationLogic({}); 115 | res.id = 'mockId2'; 116 | expect(controller.findOne('mockId2')).toEqual({ ...res }); 117 | }); 118 | 119 | it('update() | Should return "ConversationLogic" object', () => { 120 | const res = new mockConversationLogic({ 121 | id: 'mockId', 122 | name: 'mockName1', 123 | }); 124 | expect(controller.update('mockId', { name: 'mockName1' })).toEqual({ 125 | ...res, 126 | }); 127 | }); 128 | 129 | it('remove() | Should return "ConversationLogic" object', () => { 130 | const res = new mockConversationLogic({ id: 'mockId2' }); 131 | expect(controller.remove('mockId2')).toEqual({ ...res }); 132 | }); 133 | }); 134 | }); -------------------------------------------------------------------------------- /src/modules/user-segment/fusionauth/queryBuilder.ts: -------------------------------------------------------------------------------- 1 | const filterSample = { 2 | userLocation: [ 3 | { 4 | state: 'Haryana', 5 | district: 'Ambala', 6 | }, 7 | { 8 | state: 'Haryana', 9 | district: 'Panipat', 10 | block: 'Panipat', 11 | }, 12 | ], 13 | roles: ['PUBLIC'], 14 | userType: { 15 | type: 'student', 16 | }, 17 | framework: { 18 | board: ['State Board 3'], 19 | gradeLevel: [1], 20 | }, 21 | }; 22 | 23 | export class QueryBuilder { 24 | filters: any; 25 | prefixSingle: string; 26 | postfixSingle: string; 27 | ORChar: string; 28 | ANDChar: string; 29 | dataPrefix: string; 30 | fieldSeparator: string; 31 | keySeparator: string; 32 | roleKey: string; 33 | locationKey: string; 34 | frameworkKey: string; 35 | userTypeKey: string; 36 | quotes: string; 37 | 38 | constructor(filters) { 39 | this.filters = filters; 40 | this.prefixSingle = '('; 41 | this.postfixSingle = ')'; 42 | this.ORChar = ' OR '; 43 | this.ANDChar = ' AND '; 44 | this.dataPrefix = 'data'; 45 | this.fieldSeparator = '.'; 46 | this.keySeparator = ' : '; 47 | this.roleKey = 'roles'; 48 | this.locationKey = 'userLocation'; 49 | this.frameworkKey = 'framework'; 50 | this.userTypeKey = 'userType.type'; 51 | this.quotes = "'"; 52 | } 53 | 54 | checkBrackets = (str) => { 55 | // depth of the parenthesis 56 | // ex : ( 1 ( 2 ) ( 2 ( 3 ) ) ) 57 | let depth = 0; 58 | // for each char in the string : 2 cases 59 | for (const i in str) { 60 | if (str[i] == '(') { 61 | // if the char is an opening parenthesis then we increase the depth 62 | depth++; 63 | } else if (str[i] == ')') { 64 | // if the char is an closing parenthesis then we decrease the depth 65 | depth--; 66 | } 67 | // if the depth is negative we have a closing parenthesis 68 | // before any matching opening parenthesis 69 | if (depth < 0) return false; 70 | } 71 | // If the depth is not null then a closing parenthesis is missing 72 | if (depth > 0) return false; 73 | // OK ! 74 | return true; 75 | }; 76 | 77 | buildLocation = () => { 78 | return { 79 | query: this.filters.userLocation 80 | .map((location) => { 81 | const d = [] as any[]; 82 | Object.entries(location).forEach(([key, value]) => { 83 | d.push( 84 | this.prefixSingle + 85 | this.dataPrefix + 86 | this.fieldSeparator + 87 | this.locationKey + 88 | this.fieldSeparator + 89 | key + 90 | this.keySeparator + 91 | this.quotes + 92 | value + 93 | this.quotes + 94 | this.postfixSingle, 95 | ); 96 | }); 97 | return d.join(this.ANDChar); 98 | }) 99 | .map((s) => this.prefixSingle + s + this.postfixSingle) 100 | .join(this.ORChar), 101 | total: this.filters.userLocation.length, 102 | }; 103 | }; 104 | 105 | buildRoles = () => { 106 | return { 107 | query: this.filters.roles 108 | .map((role) => { 109 | return ( 110 | this.prefixSingle + 111 | this.dataPrefix + 112 | this.fieldSeparator + 113 | this.roleKey + 114 | this.keySeparator + 115 | role + 116 | this.postfixSingle 117 | ); 118 | }) 119 | .join(this.ORChar), 120 | total: this.filters.roles.length, 121 | }; 122 | }; 123 | 124 | buildUserTypes = () => { 125 | if ( 126 | this.filters.userType !== undefined && 127 | this.filters.userType.type !== undefined 128 | ) { 129 | return { 130 | query: 131 | this.prefixSingle + 132 | this.dataPrefix + 133 | this.fieldSeparator + 134 | this.userTypeKey + 135 | this.keySeparator + 136 | this.filters.userType.type + 137 | this.postfixSingle, 138 | total: 1, 139 | }; 140 | } else { 141 | return { 142 | query: '', 143 | total: 0, 144 | }; 145 | } 146 | }; 147 | 148 | buildFramework = () => { 149 | return { 150 | query: '', 151 | total: 0, 152 | }; 153 | }; 154 | 155 | buildQuery = () => { 156 | const queries = [ 157 | this.buildLocation(), 158 | this.buildRoles(), 159 | this.buildUserTypes(), 160 | this.buildFramework(), 161 | ]; 162 | const query = queries 163 | .filter((s) => s.total > 0) 164 | .map((s) => { 165 | if (s.total > 1) 166 | return this.prefixSingle + s.query + this.postfixSingle; 167 | else return s.query; 168 | }) 169 | .join(this.ANDChar); 170 | return this.checkBrackets(query) ? query : ''; 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /src/health/health.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HealthService } from './health.service'; 3 | import { PrismaService } from '../global-services/prisma.service'; 4 | import { FormService } from '../modules/form/form.service'; 5 | import { HealthCheckError, HealthIndicatorResult, HttpHealthIndicator, TerminusModule } from '@nestjs/terminus'; 6 | import { ConfigService } from '@nestjs/config'; 7 | import { TelemetryService } from '../global-services/telemetry.service'; 8 | 9 | class MockHttpHealthIndicatorUp { 10 | pingCheck(url: string, timeout: number): Promise { 11 | return Promise.resolve({ 12 | [url]: { 13 | status: 'up', 14 | }, 15 | }); 16 | } 17 | } 18 | 19 | class MockFormServiceUp { 20 | login(): Promise { 21 | return Promise.resolve(); 22 | } 23 | } 24 | 25 | class MockPrismaServiceUp { 26 | $queryRaw(): Promise { 27 | return Promise.resolve(); 28 | } 29 | } 30 | 31 | describe.skip('HealthService UP checks', () => { 32 | let healthService: HealthService; 33 | let primsmaService: PrismaService; 34 | let formService: FormService; 35 | 36 | beforeEach(async () => { 37 | const module: TestingModule = await Test.createTestingModule({ 38 | imports: [ 39 | TerminusModule 40 | ], 41 | providers: [ 42 | HealthService, { 43 | provide: HttpHealthIndicator, 44 | useClass: MockHttpHealthIndicatorUp, 45 | }, 46 | FormService, { 47 | provide: FormService, 48 | useClass: MockFormServiceUp, 49 | }, 50 | PrismaService, { 51 | provide: PrismaService, 52 | useClass: MockPrismaServiceUp, 53 | }, 54 | ConfigService, 55 | TelemetryService, 56 | ], 57 | }).compile(); 58 | 59 | healthService = module.get(HealthService); 60 | primsmaService = module.get(PrismaService); 61 | formService = module.get(FormService); 62 | }); 63 | 64 | it('uci core health up check', async () => { 65 | expect(await healthService.checkUciCoreHealth()).toEqual({ 66 | UCI_CORE: { 67 | status: 'up', 68 | } 69 | }); 70 | }); 71 | 72 | it('form service health up check', async () => { 73 | expect(await healthService.checkFormServiceHealth()).toEqual({ 74 | FormService: { 75 | status: 'up', 76 | } 77 | }); 78 | }); 79 | 80 | it('prisma service health up check', async () => { 81 | expect(await healthService.checkDatabaseHealth()).toEqual({ 82 | PrismaService: { 83 | status: 'up', 84 | } 85 | }); 86 | }); 87 | }); 88 | 89 | class MockHttpHealthIndicatorDown { 90 | pingCheck(url: string, timeout: number): Promise { 91 | return Promise.resolve({ 92 | [url]: { 93 | status: 'down', 94 | message: 'test down message', 95 | }, 96 | }); 97 | } 98 | } 99 | 100 | class MockFormServiceDown { 101 | login(): Promise { 102 | return Promise.reject('test down message'); 103 | } 104 | } 105 | 106 | class MockPrismaServiceDown { 107 | $queryRaw(): Promise { 108 | return Promise.reject('test down message'); 109 | } 110 | } 111 | 112 | describe.skip('HealthService DOWN checks', () => { 113 | let healthService: HealthService; 114 | let primsmaService: PrismaService; 115 | let formService: FormService; 116 | 117 | beforeEach(async () => { 118 | const module: TestingModule = await Test.createTestingModule({ 119 | imports: [ 120 | TerminusModule 121 | ], 122 | providers: [ 123 | HealthService, { 124 | provide: HttpHealthIndicator, 125 | useClass: MockHttpHealthIndicatorDown, 126 | }, 127 | FormService, { 128 | provide: FormService, 129 | useClass: MockFormServiceDown, 130 | }, 131 | PrismaService, { 132 | provide: PrismaService, 133 | useClass: MockPrismaServiceDown, 134 | }, 135 | ConfigService, 136 | TelemetryService, 137 | ], 138 | }).compile(); 139 | 140 | healthService = module.get(HealthService); 141 | primsmaService = module.get(PrismaService); 142 | formService = module.get(FormService); 143 | }); 144 | 145 | it('uci core health down check', async () => { 146 | expect(await healthService.checkUciCoreHealth()).toEqual({ 147 | UCI_CORE: { 148 | status: 'down', 149 | message: 'test down message', 150 | } 151 | }); 152 | }); 153 | 154 | it('form service health down check', async () => { 155 | expect(healthService.checkFormServiceHealth()).rejects.toThrow(HealthCheckError); 156 | }); 157 | 158 | it('prisma service health down check', async () => { 159 | expect(healthService.checkDatabaseHealth()).rejects.toThrow(HealthCheckError); 160 | }); 161 | }); 162 | --------------------------------------------------------------------------------