-
-
Notifications
You must be signed in to change notification settings - Fork 18
Selfosted operations module #1553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
|
||
| export class AddedCascadeOptionToAiChatEntities1770043047971 implements MigrationInterface { | ||
| name = 'AddedCascadeOptionToAiChatEntities1770043047971'; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "ai_chat_message" DROP CONSTRAINT "FK_03bc49058afd5262d6a503bf123"`); | ||
| await queryRunner.query(`ALTER TABLE "user_ai_chat" DROP CONSTRAINT "FK_0f95dbd767d42e637345636cb5d"`); | ||
| await queryRunner.query( | ||
| `ALTER TABLE "ai_chat_message" ADD CONSTRAINT "FK_03bc49058afd5262d6a503bf123" FOREIGN KEY ("ai_chat_id") REFERENCES "user_ai_chat"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, | ||
| ); | ||
| await queryRunner.query( | ||
| `ALTER TABLE "user_ai_chat" ADD CONSTRAINT "FK_0f95dbd767d42e637345636cb5d" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, | ||
| ); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "user_ai_chat" DROP CONSTRAINT "FK_0f95dbd767d42e637345636cb5d"`); | ||
| await queryRunner.query(`ALTER TABLE "ai_chat_message" DROP CONSTRAINT "FK_03bc49058afd5262d6a503bf123"`); | ||
| await queryRunner.query( | ||
| `ALTER TABLE "user_ai_chat" ADD CONSTRAINT "FK_0f95dbd767d42e637345636cb5d" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, | ||
| ); | ||
| await queryRunner.query( | ||
| `ALTER TABLE "ai_chat_message" ADD CONSTRAINT "FK_03bc49058afd5262d6a503bf123" FOREIGN KEY ("ai_chat_id") REFERENCES "user_ai_chat"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
|
||
| export class AddCascadeOptionToConnectionEntity1770045005400 implements MigrationInterface { | ||
| name = 'AddCascadeOptionToConnectionEntity1770045005400'; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "connection" DROP CONSTRAINT "FK_3c56723750fad39864878239cf4"`); | ||
| await queryRunner.query( | ||
| `ALTER TABLE "connection" ADD CONSTRAINT "FK_3c56723750fad39864878239cf4" FOREIGN KEY ("companyId") REFERENCES "company_info"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, | ||
| ); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "connection" DROP CONSTRAINT "FK_3c56723750fad39864878239cf4"`); | ||
| await queryRunner.query( | ||
| `ALTER TABLE "connection" ADD CONSTRAINT "FK_3c56723750fad39864878239cf4" FOREIGN KEY ("companyId") REFERENCES "company_info"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export class CreateInitialUserDs { | ||
| email: string; | ||
| password: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
| import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; | ||
|
|
||
| export class CreateInitialUserDto { | ||
| @ApiProperty({ description: 'User email' }) | ||
| @IsNotEmpty() | ||
| @IsString() | ||
| @IsEmail() | ||
| readonly email: string; | ||
|
|
||
| @ApiProperty({ description: 'Admin user password' }) | ||
| @IsNotEmpty() | ||
| @IsString() | ||
| @MinLength(8) | ||
| @MaxLength(255) | ||
| readonly password: string; | ||
|
Comment on lines
+14
to
+16
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
|
||
|
|
||
| export class IsConfiguredRo { | ||
| @ApiProperty({ example: true, description: 'Indicates whether the self-hosted instance is configured' }) | ||
| public isConfigured: boolean; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { BadRequestException, Inject, Injectable } from '@nestjs/common'; | ||
| import AbstractUseCase from '../../../common/abstract-use.case.js'; | ||
| import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; | ||
| import { BaseType } from '../../../common/data-injection.tokens.js'; | ||
| import { SimpleFoundUserInfoDs } from '../../../entities/user/dto/found-user.dto.js'; | ||
| import { ICreateInitialUserUseCase } from './selfhosted-use-cases.interfaces.js'; | ||
| import { CreateInitialUserDs } from '../data-structures/create-initial-user.ds.js'; | ||
| import { isSaaS } from '../../../helpers/app/is-saas.js'; | ||
| import { Messages } from '../../../exceptions/text/messages.js'; | ||
| import { RegisterUserDs } from '../../../entities/user/application/data-structures/register-user-ds.js'; | ||
| import { UserRoleEnum } from '../../../entities/user/enums/user-role.enum.js'; | ||
| import { buildRegisteringUser } from '../../../entities/user/utils/build-registering-user.util.js'; | ||
| import { CompanyInfoEntity } from '../../../entities/company-info/company-info.entity.js'; | ||
| import { Encryptor } from '../../../helpers/encryption/encryptor.js'; | ||
| import { buildSimpleUserInfoDs } from '../../../entities/user/utils/build-created-user.ds.js'; | ||
|
|
||
| @Injectable() | ||
| export class CreateInitialUserUseCase | ||
| extends AbstractUseCase<CreateInitialUserDs, SimpleFoundUserInfoDs> | ||
| implements ICreateInitialUserUseCase | ||
| { | ||
| constructor( | ||
| @Inject(BaseType.GLOBAL_DB_CONTEXT) | ||
| protected _dbContext: IGlobalDatabaseContext, | ||
| ) { | ||
| super(); | ||
| } | ||
|
|
||
| protected async implementation(inputData: CreateInitialUserDs): Promise<SimpleFoundUserInfoDs> { | ||
| if (isSaaS()) { | ||
| throw new BadRequestException(Messages.ENDPOINT_NOT_AVAILABLE_IN_THIS_MODE); | ||
| } | ||
|
|
||
| const userCount = await this._dbContext.userRepository.count(); | ||
| if (userCount > 0) { | ||
| throw new BadRequestException(Messages.SELF_HOSTED_ALREADY_CONFIGURED); | ||
| } | ||
|
|
||
| const { email, password } = inputData; | ||
| const registerUserData: RegisterUserDs = { | ||
| email: email, | ||
| password: password, | ||
| isActive: true, | ||
| gclidValue: null, | ||
| name: 'Admin', | ||
| role: UserRoleEnum.ADMIN, | ||
| }; | ||
|
|
||
| const savedUser = await this._dbContext.userRepository.saveUserEntity(buildRegisteringUser(registerUserData)); | ||
|
|
||
| const newCompanyInfo = new CompanyInfoEntity(); | ||
| newCompanyInfo.id = Encryptor.generateUUID(); | ||
| const savedCompanyInfo = await this._dbContext.companyInfoRepository.save(newCompanyInfo); | ||
|
|
||
| savedUser.company = savedCompanyInfo; | ||
| const finalUser = await this._dbContext.userRepository.saveUserEntity(savedUser); | ||
|
|
||
| return buildSimpleUserInfoDs(finalUser); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import AbstractUseCase from '../../../common/abstract-use.case.js'; | ||||||
| import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; | ||||||
| import { BaseType } from '../../../common/data-injection.tokens.js'; | ||||||
| import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js'; | ||||||
|
||||||
| import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js'; | |
| import { IsConfiguredRo } from '../response-objects/is-configured.ro.js'; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||
| import { InTransactionEnum } from '../../../enums/index.js'; | ||||||
| import { SimpleFoundUserInfoDs } from '../../../entities/user/dto/found-user.dto.js'; | ||||||
| import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js'; | ||||||
|
||||||
| import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js'; | |
| import { IsConfiguredRo } from '../response-objects/is-configured.ro.js'; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||
| import { Body, Controller, Get, HttpStatus, Inject, Post, UseInterceptors } from '@nestjs/common'; | ||||||
| import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; | ||||||
| import { SentryInterceptor } from '../interceptors/index.js'; | ||||||
| import { IsConfiguredRo } from './application/responce-objects/is-configured.ro.js'; | ||||||
|
||||||
| import { IsConfiguredRo } from './application/responce-objects/is-configured.ro.js'; | |
| import { IsConfiguredRo } from './application/response-objects/is-configured.ro.js'; |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating the initial user involves multiple database operations (creating a user, creating a company, and linking them). This critical operation should use InTransactionEnum.ON instead of InTransactionEnum.OFF to ensure atomicity. If any step fails (e.g., company creation fails after user creation), without a transaction you could end up with an orphan user entity and inconsistent database state. This pattern is used for other multi-step create operations in the codebase (e.g., backend/src/entities/connection/connection.controller.ts:285).
| return await this.createInitialUserUseCase.execute(createInitialUserDto, InTransactionEnum.OFF); | |
| return await this.createInitialUserUseCase.execute(createInitialUserDto, InTransactionEnum.ON); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { DynamicModule, Module } from '@nestjs/common'; | ||
|
||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||
| import { UserEntity } from '../entities/user/user.entity.js'; | ||
| import { CompanyInfoEntity } from '../entities/company-info/company-info.entity.js'; | ||
| import { SelfHostedOperationsController } from './selfhosted-operations.controller.js'; | ||
| import { GlobalDatabaseContext } from '../common/application/global-database-context.js'; | ||
| import { BaseType, UseCaseType } from '../common/data-injection.tokens.js'; | ||
| import { IsConfiguredUseCase } from './application/use-cases/is-configured.use.case.js'; | ||
| import { CreateInitialUserUseCase } from './application/use-cases/create-initial-user.use.case.js'; | ||
| import { isSaaS } from '../helpers/app/is-saas.js'; | ||
|
|
||
| @Module({}) | ||
| export class SelfHostedOperationsModule { | ||
| static register(): DynamicModule { | ||
| if (isSaaS()) { | ||
| // Return empty module in SaaS mode | ||
| return { | ||
| module: SelfHostedOperationsModule, | ||
| imports: [], | ||
| controllers: [], | ||
| providers: [], | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| module: SelfHostedOperationsModule, | ||
| imports: [TypeOrmModule.forFeature([UserEntity, CompanyInfoEntity])], | ||
| controllers: [SelfHostedOperationsController], | ||
| providers: [ | ||
| { | ||
| provide: BaseType.GLOBAL_DB_CONTEXT, | ||
| useClass: GlobalDatabaseContext, | ||
| }, | ||
| { | ||
| provide: UseCaseType.IS_CONFIGURED, | ||
| useClass: IsConfiguredUseCase, | ||
| }, | ||
| { | ||
| provide: UseCaseType.CREATE_INITIAL_USER, | ||
| useClass: CreateInitialUserUseCase, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import path contains a typo - "selhosted-operations.module.js" should be "selfhosted-operations.module.js" (missing the 'f'). This needs to be corrected along with the actual filename.