diff --git a/api/package.json b/api/package.json index f5b370fe..97cac5c5 100644 --- a/api/package.json +++ b/api/package.json @@ -5,6 +5,11 @@ "description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.", "author": "Hexastack", "license": "AGPL-3.0-only", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "postinstall": "patch-package", "build:clean": "rm -rf src/.hexabot", @@ -13,7 +18,8 @@ "build:plugins": "mkdir -p src/.hexabot/extensions/plugins && find node_modules/ -name 'hexabot-plugin-*' -exec cp -R {} src/.hexabot/extensions/plugins/ \\;", "build:extensions": "npm run build:channels && npm run build:helpers && npm run build:plugins", "build:prepare": "npm run build:clean && npm run build:extensions", - "build": "npm run build:prepare && nest build", + "build": "npm run build:prepare && nest build && npm run copy-types", + "copy-types": "cp -R types dist/types", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", "start": "nest start", "doc": "npx @compodoc/compodoc --hideGenerator -p tsconfig.doc.json -s -r 9003 -w", diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 6fe1bf55..6d5cbf1f 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -136,4 +136,4 @@ const i18nOptions: I18nOptions = { AppService, ], }) -export class AppModule {} +export class HexabotModule {} diff --git a/api/src/channel/lib/Handler.ts b/api/src/channel/lib/Handler.ts index 4b265b23..2c8d2e0f 100644 --- a/api/src/channel/lib/Handler.ts +++ b/api/src/channel/lib/Handler.ts @@ -20,6 +20,7 @@ import { import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; import { Extension } from '@/utils/generics/extension'; +import { HyphenToUnderscore } from '@/utils/types/extension'; import { SocketRequest } from '@/websocket/utils/socket-request'; import { SocketResponse } from '@/websocket/utils/socket-response'; diff --git a/api/src/channel/types.ts b/api/src/channel/types.ts index c9174321..0c654f19 100644 --- a/api/src/channel/types.ts +++ b/api/src/channel/types.ts @@ -7,6 +7,7 @@ */ import { SettingCreateDto } from '@/setting/dto/setting.dto'; +import { HyphenToUnderscore } from '@/utils/types/extension'; export type ChannelName = `${string}-channel`; diff --git a/api/src/config/index.ts b/api/src/config/index.ts index 03001f48..d81992d3 100644 --- a/api/src/config/index.ts +++ b/api/src/config/index.ts @@ -134,12 +134,12 @@ export const config: Config = { max: 100, // Maximum number of items in cache (defaults to 100) }, mongo: { - user: process.env.MONGO_USER, - password: process.env.MONGO_PASSWORD, - uri: process.env.MONGO_URI, - dbName: process.env.MONGO_DB, + user: process.env.MONGO_USER || 'dev_only', + password: process.env.MONGO_PASSWORD || 'dev_only', + uri: process.env.MONGO_URI || 'mongodb://dev_only:dev_only@mongo:27017/', + dbName: process.env.MONGO_DB || 'hexabot', }, - env: process.env.NODE_ENV, + env: process.env.NODE_ENV || 'development', authentication: { jwtOptions: { salt: parseInt(process.env.SALT_LENGTH || '12'), diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index 59e70f07..2ff785df 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -299,7 +299,7 @@ export default abstract class BaseWebChannelHandler< */ private async verifyToken(verificationToken: string) { const settings = - (await this.getSettings()) as Settings[typeof WEB_CHANNEL_NAMESPACE]; + (await this.getSettings()) as unknown as Settings[typeof WEB_CHANNEL_NAMESPACE]; const verifyToken = settings.verification_token; if (!verifyToken) { diff --git a/api/src/global.d.ts b/api/src/global.d.ts new file mode 100644 index 00000000..17568c39 --- /dev/null +++ b/api/src/global.d.ts @@ -0,0 +1,16 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +declare global { + type HyphenToUnderscore = S extends `${infer P}-${infer Q}` + ? `${P}_${HyphenToUnderscore}` + : S; +} + +// eslint-disable-next-line prettier/prettier +export { }; diff --git a/api/src/helper/lib/base-helper.ts b/api/src/helper/lib/base-helper.ts index 4ef91ad0..1c46f0ac 100644 --- a/api/src/helper/lib/base-helper.ts +++ b/api/src/helper/lib/base-helper.ts @@ -12,6 +12,7 @@ import { LoggerService, OnModuleInit } from '@nestjs/common'; import { SettingService } from '@/setting/services/setting.service'; import { Extension } from '@/utils/generics/extension'; +import { HyphenToUnderscore } from '@/utils/types/extension'; import { HelperService } from '../helper.service'; import { HelperName, HelperSetting, HelperType } from '../types'; diff --git a/api/src/helper/types.ts b/api/src/helper/types.ts index 7692c5a9..2e6a7256 100644 --- a/api/src/helper/types.ts +++ b/api/src/helper/types.ts @@ -7,6 +7,7 @@ */ import { SettingCreateDto } from '@/setting/dto/setting.dto'; +import { HyphenToUnderscore } from '@/utils/types/extension'; import BaseHelper from './lib/base-helper'; import BaseLlmHelper from './lib/base-llm-helper'; diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 00000000..9770e4f7 --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +export * from './app.module'; diff --git a/api/src/main.ts b/api/src/main.ts index afddfe8d..2572d9b5 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -18,19 +18,19 @@ moduleAlias.addAliases({ '@': __dirname, }); -import { AppModule } from './app.module'; +import { HexabotModule } from './app.module'; import { config } from './config'; import { LoggerService } from './logger/logger.service'; import { seedDatabase } from './seeder'; import { swagger } from './swagger'; -import { sessionStore } from './utils/constants/session-store'; +import { getSessionStore } from './utils/constants/session-store'; import { ObjectIdPipe } from './utils/pipes/object-id.pipe'; async function bootstrap() { const isProduction = config.env.toLowerCase().includes('prod'); await resolveDynamicProviders(); - const app = await NestFactory.create(AppModule, { + const app = await NestFactory.create(HexabotModule, { bodyParser: false, }); @@ -63,7 +63,7 @@ async function bootstrap() { proxy: config.security.trustProxy, resave: true, saveUninitialized: false, - store: sessionStore, + store: getSessionStore(), cookie: { httpOnly: true, secure: config.security.httpsEnabled, diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index 7e8465cd..2b2bcc82 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -8,6 +8,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { DeleteResult } from '@/utils/generics/base-repository'; import { BaseService } from '@/utils/generics/base-service'; import { NlpValueCreateDto, NlpValueUpdateDto } from '../dto/nlp-value.dto'; @@ -43,7 +44,7 @@ export class NlpValueService extends BaseService< * * @returns A promise that resolves when the deletion is complete. */ - async deleteCascadeOne(id: string) { + async deleteCascadeOne(id: string): Promise { return await this.repository.deleteOne(id); } diff --git a/api/src/user/repositories/model.repository.ts b/api/src/user/repositories/model.repository.ts index 73c9193e..9080cc31 100644 --- a/api/src/user/repositories/model.repository.ts +++ b/api/src/user/repositories/model.repository.ts @@ -11,7 +11,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { InjectModel } from '@nestjs/mongoose'; import { Model as MongooseModel } from 'mongoose'; -import { BaseRepository } from '@/utils/generics/base-repository'; +import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { Model, @@ -43,7 +43,7 @@ export class ModelRepository extends BaseRepository< * * @returns The result of the delete operation. */ - async deleteOne(id: string) { + async deleteOne(id: string): Promise { const result = await this.model.deleteOne({ _id: id }).exec(); if (result.deletedCount > 0) { await this.permissionModel.deleteMany({ model: id }); diff --git a/api/src/user/repositories/role.repository.ts b/api/src/user/repositories/role.repository.ts index 27431711..2208fa90 100644 --- a/api/src/user/repositories/role.repository.ts +++ b/api/src/user/repositories/role.repository.ts @@ -11,7 +11,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { BaseRepository } from '@/utils/generics/base-repository'; +import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { Permission } from '../schemas/permission.schema'; import { @@ -43,7 +43,7 @@ export class RoleRepository extends BaseRepository< * * @returns The result of the delete operation. */ - async deleteOne(id: string) { + async deleteOne(id: string): Promise { const result = await this.model.deleteOne({ _id: id }).exec(); if (result.deletedCount > 0) { await this.permissionModel.deleteMany({ role: id }); diff --git a/api/src/user/services/model.service.ts b/api/src/user/services/model.service.ts index 7287a324..60f5e648 100644 --- a/api/src/user/services/model.service.ts +++ b/api/src/user/services/model.service.ts @@ -18,15 +18,4 @@ export class ModelService extends BaseService { constructor(readonly repository: ModelRepository) { super(repository); } - - /** - * Deletes a Model entity by its unique identifier. - * - * @param id - The unique identifier of the Model entity to delete. - * - * @returns A promise that resolves to the result of the deletion operation. - */ - async deleteOne(id: string) { - return await this.repository.deleteOne(id); - } } diff --git a/api/src/utils/constants/session-store.ts b/api/src/utils/constants/session-store.ts index acf9861b..8f559304 100644 --- a/api/src/utils/constants/session-store.ts +++ b/api/src/utils/constants/session-store.ts @@ -10,8 +10,15 @@ import MongoStore from 'connect-mongo'; import { config } from '@/config'; -export const sessionStore = MongoStore.create({ - mongoUrl: config.mongo.uri, - dbName: config.mongo.dbName, - collectionName: 'sessions', -}); +let sessionStore: MongoStore = null; + +export const getSessionStore = () => { + if (!sessionStore) { + sessionStore = MongoStore.create({ + mongoUrl: config.mongo.uri, + dbName: config.mongo.dbName, + collectionName: 'sessions', + }); + } + return sessionStore; +}; diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 8db29872..03373087 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -340,13 +340,13 @@ export abstract class BaseRepository< }); } - async deleteOne(criteria: string | TFilterQuery) { + async deleteOne(criteria: string | TFilterQuery): Promise { return await this.model .deleteOne(typeof criteria === 'string' ? { _id: criteria } : criteria) .exec(); } - async deleteMany(criteria: TFilterQuery) { + async deleteMany(criteria: TFilterQuery): Promise { return await this.model.deleteMany(criteria); } diff --git a/api/src/utils/generics/extension.ts b/api/src/utils/generics/extension.ts index 7b530f7f..5d0b3be9 100644 --- a/api/src/utils/generics/extension.ts +++ b/api/src/utils/generics/extension.ts @@ -13,7 +13,7 @@ import { OnModuleInit } from '@nestjs/common'; import { I18nJsonLoader, I18nTranslation } from 'nestjs-i18n'; import { Observable } from 'rxjs'; -import { ExtensionName } from '../types/extension'; +import { ExtensionName, HyphenToUnderscore } from '../types/extension'; export abstract class Extension implements OnModuleInit { private translations: I18nTranslation | Observable; diff --git a/api/src/utils/types/extension.ts b/api/src/utils/types/extension.ts index 07ac62d5..feda082d 100644 --- a/api/src/utils/types/extension.ts +++ b/api/src/utils/types/extension.ts @@ -11,3 +11,6 @@ import { HelperName } from '@/helper/types'; import { PluginName } from '@/plugins/types'; export type ExtensionName = ChannelName | HelperName | PluginName; + +export type HyphenToUnderscore = + S extends `${infer P}-${infer Q}` ? `${P}_${HyphenToUnderscore}` : S; diff --git a/api/src/websocket/websocket.gateway.ts b/api/src/websocket/websocket.gateway.ts index f556c609..788b5a94 100644 --- a/api/src/websocket/websocket.gateway.ts +++ b/api/src/websocket/websocket.gateway.ts @@ -33,7 +33,7 @@ import { import { OutgoingMessage, StdEventType } from '@/chat/schemas/types/message'; import { config } from '@/config'; import { LoggerService } from '@/logger/logger.service'; -import { sessionStore } from '@/utils/constants/session-store'; +import { getSessionStore } from '@/utils/constants/session-store'; import { IOIncomingMessage, IOMessagePipe } from './pipes/io-message.pipe'; import { SocketEventDispatcherService } from './services/socket-event-dispatcher.service'; @@ -134,7 +134,7 @@ export class WebsocketGateway }, passport: { user: {} }, }; // Initialize your session object as needed - sessionStore.set(sid, newSession, (err) => { + getSessionStore().set(sid, newSession, (err) => { if (err) { this.logger.error('Error saving session:', err); return next(new Error('Unable to establish a new socket session')); @@ -179,7 +179,7 @@ export class WebsocketGateway ); return; } - sessionStore.set(sessionID, session, (err) => { + getSessionStore().set(sessionID, session, (err) => { if (err) { this.logger.error( 'Error saving session in `config.sockets.afterDisconnect`:', @@ -195,7 +195,7 @@ export class WebsocketGateway sessionID: string, next: (err: Error, session: any) => void, ): void { - sessionStore.get(sessionID, (err, session) => { + getSessionStore().get(sessionID, (err, session) => { this.logger.verbose('Retrieved socket session', err || session); return next(err, session); }); diff --git a/api/test/app.e2e-spec.ts b/api/test/app.e2e-spec.ts index 2f74dc12..8aa73c07 100644 --- a/api/test/app.e2e-spec.ts +++ b/api/test/app.e2e-spec.ts @@ -10,14 +10,14 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; -import { AppModule } from './../src/app.module'; +import { HexabotModule } from './../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], + imports: [HexabotModule], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/api/tsconfig.json b/api/tsconfig.json index 27724e72..b5906aeb 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "typeRoots": ["./node_modules/@types", "./types"], "module": "commonjs", "declaration": true, "removeComments": true, @@ -24,6 +25,8 @@ } }, "include": [ + "types/**/*.d.ts", + "src/global.d.ts", "src/**/*.ts", "src/**/*.json", "test/**/*.ts", diff --git a/api/src/eventemitter.d.ts b/api/types/event-emitter.d.ts similarity index 96% rename from api/src/eventemitter.d.ts rename to api/types/event-emitter.d.ts index b84bce90..9f08b65f 100644 --- a/api/src/eventemitter.d.ts +++ b/api/types/event-emitter.d.ts @@ -19,6 +19,7 @@ import { type Socket } from 'socket.io'; import { type BotStats } from '@/analytics/schemas/bot-stats.schema'; import { type Attachment } from '@/attachment/schemas/attachment.schema'; import type EventWrapper from '@/channel/lib/EventWrapper'; +import { type SubscriberUpdateDto } from '@/chat/dto/subscriber.dto'; import type { Block, BlockFull } from '@/chat/schemas/block.schema'; import { type Category } from '@/chat/schemas/category.schema'; import { type ContextVar } from '@/chat/schemas/context-var.schema'; @@ -49,8 +50,6 @@ import { type Role } from '@/user/schemas/role.schema'; import { type User } from '@/user/schemas/user.schema'; import { EHook, type DeleteResult } from '@/utils/generics/base-repository'; -import { type SubscriberUpdateDto } from './chat/dto/subscriber.dto'; - import '@nestjs/event-emitter'; /** * @description Module declaration that extends the NestJS EventEmitter with custom event types and methods. @@ -87,15 +86,6 @@ declare module '@nestjs/event-emitter' { company_country: Setting; } >; - nlp_settings: TDefinition< - object, - { - provider: Setting; - endpoint: Setting; - token: Setting; - threshold: Setting; - } - >; } /* custom hooks */ @@ -187,12 +177,16 @@ declare module '@nestjs/event-emitter' { * @description A constrained string type that allows specific string values while preserving type safety. */ type ConstrainedString = string & Record; + type EventNamespaces = keyof IHookEntityOperationMap; /* pre hooks */ type TPreValidate = THydratedDocument; + type TPreCreate = THydratedDocument; + type TPreUpdate = TFilterQuery & object; + type TPreDelete = Query< DeleteResult, Document, @@ -201,6 +195,7 @@ declare module '@nestjs/event-emitter' { 'deleteOne', Record >; + type TPreUnion = | TPreValidate | TPreCreate @@ -209,9 +204,13 @@ declare module '@nestjs/event-emitter' { /* post hooks */ type TPostValidate = THydratedDocument; + type TPostCreate = THydratedDocument; + type TPostUpdate = THydratedDocument; + type TPostDelete = DeleteResult; + type TPostUnion = | TPostValidate | TPostCreate @@ -240,6 +239,7 @@ declare module '@nestjs/event-emitter' { > = T extends `${P}${infer I}` ? `${P}${I}` : never; type TPreHook = TCompatibleHook; + type TPostHook = TCompatibleHook; type TNormalizedEvents = '*' | TPreHook | TPostHook; @@ -313,11 +313,11 @@ declare module '@nestjs/event-emitter' { : `hook:${G}:${TNormalizedEvents | TCustomEvents}` : never; - export interface ListenerFn { + interface ListenerFn { (value: EventValueOf, ...values: any[]): void; } - export class EventEmitter2 { + class EventEmitter2 { emit( customEvent: customEvent, value: EventValueOf, @@ -392,7 +392,7 @@ declare module '@nestjs/event-emitter' { propertyKey: K, ) => void; - export declare function OnEvent< + declare function OnEvent< G extends EventNamespaces | ConstrainedString, H extends G, >( diff --git a/api/src/index.d.ts b/api/types/express-session.d.ts similarity index 58% rename from api/src/index.d.ts rename to api/types/express-session.d.ts index f0ec76e8..7039765a 100644 --- a/api/src/index.d.ts +++ b/api/types/express-session.d.ts @@ -6,16 +6,8 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import 'mongoose'; import { SubscriberStub } from './chat/schemas/subscriber.schema'; -import { - ObjectWithNestedKeys, - RecursivePartial, - WithoutGenericAny, -} from './utils/types/filter.types'; -type TOmitId = Omit; -type TReplaceId = TOmitId & { _id?: string }; declare module 'express-session' { interface SessionUser { id?: string; @@ -48,24 +40,3 @@ declare module 'express-session' { }; } } - -declare module 'mongoose' { - // Enforce the typing with an alternative type to FilterQuery compatible with mongoose: version 8.0.0 - type TFilterQuery> = ( - | RecursivePartial<{ - [P in keyof S]?: - | (S[P] extends string ? S[P] | RegExp : S[P]) - | QuerySelector; - }> - | Partial> - ) & - WithoutGenericAny>; - - type THydratedDocument = TOmitId>; -} - -declare global { - type HyphenToUnderscore = S extends `${infer P}-${infer Q}` - ? `${P}_${HyphenToUnderscore}` - : S; -} diff --git a/api/types/mongoose.d.ts b/api/types/mongoose.d.ts new file mode 100644 index 00000000..3101ac2e --- /dev/null +++ b/api/types/mongoose.d.ts @@ -0,0 +1,32 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { + ObjectWithNestedKeys, + RecursivePartial, + WithoutGenericAny, +} from '@/utils/types/filter.types'; +import 'mongoose'; + +declare module 'mongoose' { + type TOmitId = Omit; + type TReplaceId = TOmitId & { _id?: string }; + + // Enforce the typing with an alternative type to FilterQuery compatible with mongoose: version 8.0.0 + type TFilterQuery> = ( + | RecursivePartial<{ + [P in keyof S]?: + | (S[P] extends string ? S[P] | RegExp : S[P]) + | QuerySelector; + }> + | Partial> + ) & + WithoutGenericAny>; + + type THydratedDocument = TOmitId>; +}