From 7cb228a323a1004cbbf1e12bc0cd1f2c9de43bb8 Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Thu, 5 Mar 2026 07:35:18 +0900 Subject: [PATCH 01/11] refactoring : common base-service added, etc. --- INDEX_SIGNATURE_EXPLANATION.md | 301 ++++++++++++++++++ SERVE_WEB_MANAGER.md | 236 ++++++++++++++ apps/api-server/src/broker/broker.service.ts | 127 +++----- .../src/cms-config/cms-config.service.ts | 118 +++---- apps/api-server/src/common/base.service.ts | 72 +++++ apps/api-server/src/common/index.ts | 3 + .../backup/database-backup.controller.ts | 25 +- .../backup/database-backup.service.ts | 101 +++--- .../src/database/base-database.service.ts | 5 + .../config/database-config.controller.ts | 35 +- .../config/database-config.service.ts | 257 ++++++++------- .../src/database/database.constants.ts | 22 ++ .../database-lifecycle.controller.ts | 17 +- .../lifecycle/database-lifecycle.service.ts | 191 +++++------ .../database-management.controller.ts | 44 ++- .../management/database-management.service.ts | 217 +++++-------- .../database/user/database-user.controller.ts | 8 +- .../database/user/database-user.service.ts | 44 ++- apps/api-server/src/error/app-error.ts | 60 +++- .../src/error/broker/broker-error-code.ts | 7 + .../src/error/broker/broker-error.ts | 35 +- .../src/error/cms/cms-error-code.ts | 12 + apps/api-server/src/error/cms/cms-error.ts | 14 +- .../src/error/config/config-error.ts | 8 +- .../src/error/host/host-error-code.ts | 1 + apps/api-server/src/error/index.ts | 1 + .../error/validation/validation-error-code.ts | 6 +- apps/api-server/src/log/log.service.ts | 92 ++---- .../resource-monitoring.service.ts | 29 +- .../src/type/cms-request/base-cms-request.ts | 5 +- package.json | 5 + serve-web-manager.js | 118 +++++++ 32 files changed, 1438 insertions(+), 778 deletions(-) create mode 100644 INDEX_SIGNATURE_EXPLANATION.md create mode 100644 SERVE_WEB_MANAGER.md create mode 100644 apps/api-server/src/common/base.service.ts create mode 100644 apps/api-server/src/database/base-database.service.ts create mode 100644 apps/api-server/src/database/database.constants.ts create mode 100644 apps/api-server/src/error/cms/cms-error-code.ts create mode 100644 serve-web-manager.js diff --git a/INDEX_SIGNATURE_EXPLANATION.md b/INDEX_SIGNATURE_EXPLANATION.md new file mode 100644 index 0000000..09fdd67 --- /dev/null +++ b/INDEX_SIGNATURE_EXPLANATION.md @@ -0,0 +1,301 @@ +# TypeScript 인덱스 시그니처(Index Signature) 설명 + +## 인덱스 시그니처란? + +인덱스 시그니처는 **객체의 동적 키(dynamic key)를 타입으로 정의**하는 TypeScript의 기능입니다. + +## 기본 문법 + +```typescript +{ + [key: string]: 타입; +} +// 또는 +{ + [key: number]: 타입; +} +``` + +## 간단한 예제 + +### 예제 1: 기본 사용법 + +```typescript +// 인덱스 시그니처 사용 +type MyObject = { + [key: string]: number; +}; + +const obj: MyObject = { + "name": 1, // ✅ OK + "age": 2, // ✅ OK + "city": 3, // ✅ OK + "anyKey": 100 // ✅ OK - 어떤 문자열 키든 가능 +}; + +// obj["anything"] = 200; // ✅ OK +``` + +### 예제 2: 실제 사용 사례 + +```typescript +// 사용자 정의 속성 객체 +type UserPreferences = { + theme: string; // 명시적 속성 + language: string; // 명시적 속성 + + [key: string]: string; // 인덱스 시그니처: 나머지 모든 키는 string +}; + +const prefs: UserPreferences = { + theme: "dark", + language: "ko", + fontSize: "large", // ✅ OK - 인덱스 시그니처로 허용됨 + colorScheme: "blue" // ✅ OK +}; +``` + +## 우리 프로젝트의 경우 + +### GetBackupInfoCmsResponse 타입 + +```typescript +export type GetBackupInfoCmsResponse = BaseCmsResponse & { + dbname: string; // 명시적 속성: 데이터베이스 이름 + + // 인덱스 시그니처: 동적 키로 백업 정보 배열을 저장 + [key: string]: string | BackupInfo[]; +}; +``` + +### 실제 응답 구조 + +```json +{ + "__EXEC_TIME": "100ms", + "status": "success", + "task": "getbackupinfo", + "dbname": "demodb", // ← 명시적 속성 (string) + "demodb": [ // ← 동적 키 (BackupInfo[]) + { "backupid": "1", ... }, + { "backupid": "2", ... } + ], + "testdb": [ // ← 다른 DB도 동적 키로 가능 + { "backupid": "3", ... } + ] +} +``` + +## 문제 상황 + +### 타입 충돌 + +```typescript +type GetBackupInfoCmsResponse = { + dbname: string; // 명시적: string + + [key: string]: string | BackupInfo[]; // 인덱스 시그니처: 모든 키는 string | BackupInfo[] +}; +``` + +**문제점:** +- `dbname`은 명시적으로 `string`으로 정의했지만 +- 인덱스 시그니처 `[key: string]: string | BackupInfo[]`가 **모든 문자열 키**를 포함 +- TypeScript는 `dbname`도 인덱스 시그니처의 범위에 포함시킴 +- 결과: `dbname`의 타입이 `string | BackupInfo[]`로 추론됨 + +### 시각적 설명 + +``` +타입 정의: +┌─────────────────────────────────────────┐ +│ dbname: string │ ← 명시적 정의 +│ [key: string]: string | BackupInfo[] │ ← 인덱스 시그니처 +└─────────────────────────────────────────┘ + ↓ +TypeScript의 타입 추론: +┌─────────────────────────────────────────┐ +│ 모든 문자열 키 → string | BackupInfo[] │ +│ dbname도 문자열 키이므로 포함됨! │ +└─────────────────────────────────────────┘ + ↓ +최종 타입: +┌─────────────────────────────────────────┐ +│ dbname: string | BackupInfo[] ❌ │ ← 문제! +└─────────────────────────────────────────┘ +``` + +## 해결 방법 + +### 방법 1: 타입 단언 (현재 사용 중) + +```typescript +// extractDomainData 전에 dbname을 먼저 추출 +const responseDbname = response.dbname as string; // 명시적 타입 단언 +``` + +**장점:** +- 간단하고 명확 +- 실제로 `dbname`은 항상 `string`이므로 안전 + +**단점:** +- 타입 단언은 런타임 검증을 하지 않음 + +### 방법 2: 타입 정의 개선 (권장) + +```typescript +export type GetBackupInfoCmsResponse = BaseCmsResponse & { + dbname: string; // 명시적 속성 + + // 인덱스 시그니처를 선택적(optional)으로 만들고 dbname 제외 + [key: string]: string | BackupInfo[] | undefined; +} & { + // dbname을 명시적으로 string으로 보장 + dbname: string; +}; +``` + +또는 더 나은 방법: + +```typescript +export type GetBackupInfoCmsResponse = BaseCmsResponse & { + dbname: string; // 명시적 속성 +} & { + // dbname을 제외한 나머지 동적 키들 + [K in Exclude]?: BackupInfo[]; +}; +``` + +## 인덱스 시그니처의 특징 + +### 1. 모든 키를 포함 + +```typescript +type Example = { + name: string; + [key: string]: string | number; +}; + +// name도 인덱스 시그니처의 범위에 포함됨 +// name: string | number (string이 포함되므로 OK) +``` + +### 2. 타입 호환성 + +```typescript +type A = { + [key: string]: string | number; +}; + +type B = { + name: string; // string은 string | number에 호환됨 ✅ + age: number; // number는 string | number에 호환됨 ✅ + city: boolean; // ❌ 오류! boolean은 string | number에 포함되지 않음 +}; +``` + +### 3. 읽기 전용 인덱스 시그니처 + +```typescript +type ReadOnly = { + readonly [key: string]: string; +}; + +const obj: ReadOnly = { name: "test" }; +// obj.name = "new"; // ❌ 오류! 읽기 전용 +``` + +## 실제 사용 예제 + +### 예제 1: 설정 객체 + +```typescript +type Config = { + apiUrl: string; + timeout: number; + [key: string]: string | number; // 추가 설정 허용 +}; + +const config: Config = { + apiUrl: "https://api.example.com", + timeout: 5000, + retryCount: 3, // ✅ 동적 추가 가능 + maxConnections: 10 // ✅ 동적 추가 가능 +}; +``` + +### 예제 2: 번역 객체 + +```typescript +type Translations = { + [key: string]: string; // 모든 키가 문자열 값 +}; + +const translations: Translations = { + "hello": "안녕하세요", + "goodbye": "안녕히 가세요", + "welcome": "환영합니다" + // 어떤 키든 추가 가능 +}; +``` + +### 예제 3: 우리 프로젝트처럼 동적 키 + +```typescript +type DatabaseBackups = { + dbname: string; // 명시적 속성 + [dbName: string]: BackupInfo[]; // 동적 키: DB 이름별 백업 정보 +}; + +// 실제 데이터: +{ + dbname: "demodb", + "demodb": [backup1, backup2], + "testdb": [backup3, backup4] +} +``` + +## 주의사항 + +### 1. 타입 안전성 감소 + +인덱스 시그니처를 사용하면 타입 체크가 느슨해집니다: + +```typescript +type Loose = { + [key: string]: any; // 모든 타입 허용 +}; + +const obj: Loose = { + name: "test", + age: 30, + isValid: true +}; + +// obj.anything = "anything"; // ✅ 컴파일은 통과하지만 위험할 수 있음 +``` + +### 2. 명시적 속성과의 충돌 + +```typescript +type Conflicting = { + name: string; + [key: string]: number; // ❌ 오류! name은 string인데 인덱스 시그니처는 number +}; +``` + +**해결:** +```typescript +type Fixed = { + name: string; + [key: string]: string | number; // ✅ string과 number 모두 허용 +}; +``` + +## 요약 + +1. **인덱스 시그니처**는 동적 키를 타입으로 정의하는 기능 +2. `[key: string]: 타입` 형태로 사용 +3. **모든 문자열 키**에 적용되므로 명시적 속성과 충돌할 수 있음 +4. 우리 프로젝트에서는 `dbname`이 인덱스 시그니처에 포함되어 타입 오류 발생 +5. 해결: 타입 단언 또는 타입 정의 개선 diff --git a/SERVE_WEB_MANAGER.md b/SERVE_WEB_MANAGER.md new file mode 100644 index 0000000..0a51594 --- /dev/null +++ b/SERVE_WEB_MANAGER.md @@ -0,0 +1,236 @@ +# Web Manager 서빙 가이드 + +nginx 없이 Node.js만 사용해서 web-manager를 서빙하는 방법입니다. + +## 방법 1: 제공된 스크립트 사용 (추천) + +### 1단계: 프론트엔드 빌드 + +```bash +nx build web-manager +``` + +### 2단계: 서버 실행 + +```bash +# 기본 포트(4200)로 실행 +npm run serve:web-manager + +# 또는 직접 실행 +node serve-web-manager.js + +# 다른 포트로 실행 +node serve-web-manager.js 8080 +``` + +### 3단계: 접속 + +브라우저에서 `http://서버IP:4200` (또는 지정한 포트)로 접속하세요. + +--- + +## 방법 2: npx serve 사용 (가장 간단) + +### 1단계: 프론트엔드 빌드 + +```bash +nx build web-manager +``` + +### 2단계: 서버 실행 + +```bash +# 기본 포트(3000)로 실행 +npx serve dist/apps/web-manager + +# 특정 포트로 실행 +npx serve dist/apps/web-manager -l 4200 + +# 모든 인터페이스에서 접속 가능하게 (0.0.0.0) +npx serve dist/apps/web-manager -l tcp://0.0.0.0:4200 +``` + +### 3단계: 접속 + +브라우저에서 `http://서버IP:4200`로 접속하세요. + +--- + +## 방법 3: npx http-server 사용 + +### 1단계: 프론트엔드 빌드 + +```bash +nx build web-manager +``` + +### 2단계: 서버 실행 + +```bash +# 기본 포트(8080)로 실행 +npx http-server dist/apps/web-manager + +# 특정 포트로 실행 +npx http-server dist/apps/web-manager -p 4200 + +# 모든 인터페이스에서 접속 가능하게 +npx http-server dist/apps/web-manager -p 4200 -a 0.0.0.0 +``` + +### 3단계: 접속 + +브라우저에서 `http://서버IP:4200`로 접속하세요. + +--- + +## 방법 4: Vite Preview 사용 + +### 1단계: 프론트엔드 빌드 + +```bash +nx build web-manager +``` + +### 2단계: Preview 서버 실행 + +```bash +# vite.config.mts의 preview 설정 사용 (기본 포트 4200) +npx vite preview --outDir dist/apps/web-manager + +# 특정 포트로 실행 +npx vite preview --outDir dist/apps/web-manager --port 4200 + +# 모든 인터페이스에서 접속 가능하게 +npx vite preview --outDir dist/apps/web-manager --port 4200 --host 0.0.0.0 +``` + +### 3단계: 접속 + +브라우저에서 `http://서버IP:4200`로 접속하세요. + +--- + +## 리눅스 서버에서 백그라운드 실행 + +### 방법 1: nohup 사용 + +```bash +# 방법 1 (제공된 스크립트) +nohup node serve-web-manager.js 4200 > web-manager.log 2>&1 & + +# 방법 2 (npx serve) +nohup npx serve dist/apps/web-manager -l tcp://0.0.0.0:4200 > web-manager.log 2>&1 & + +# 방법 3 (npx http-server) +nohup npx http-server dist/apps/web-manager -p 4200 -a 0.0.0.0 > web-manager.log 2>&1 & +``` + +### 방법 2: screen 사용 + +```bash +# screen 세션 시작 +screen -S web-manager + +# 서버 실행 +node serve-web-manager.js 4200 + +# Ctrl+A, D로 detach (백그라운드로 전환) +# 다시 접속하려면: screen -r web-manager +``` + +### 방법 3: tmux 사용 + +```bash +# tmux 세션 시작 +tmux new -s web-manager + +# 서버 실행 +node serve-web-manager.js 4200 + +# Ctrl+B, D로 detach +# 다시 접속하려면: tmux attach -t web-manager +``` + +--- + +## systemd 서비스로 등록 (선택사항) + +`/etc/systemd/system/web-manager.service` 파일 생성: + +```ini +[Unit] +Description=Web Manager Frontend Server +After=network.target + +[Service] +Type=simple +User=your-username +WorkingDirectory=/path/to/cubrid-webmanager +ExecStart=/usr/bin/node /path/to/cubrid-webmanager/serve-web-manager.js 4200 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +서비스 시작: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable web-manager +sudo systemctl start web-manager +sudo systemctl status web-manager +``` + +--- + +## 방화벽 설정 + +리눅스 서버에서 포트를 열어야 할 수 있습니다: + +```bash +# UFW 사용 시 +sudo ufw allow 4200/tcp + +# firewalld 사용 시 +sudo firewall-cmd --permanent --add-port=4200/tcp +sudo firewall-cmd --reload + +# iptables 사용 시 +sudo iptables -A INPUT -p tcp --dport 4200 -j ACCEPT +``` + +--- + +## 주의사항 + +1. **프로덕션 환경**에서는 nginx나 Apache 같은 웹 서버 사용을 권장합니다. +2. **HTTPS**가 필요한 경우 nginx를 리버스 프록시로 사용하거나, Node.js에서 HTTPS를 직접 설정해야 합니다. +3. **보안**: 개발/테스트 목적으로만 사용하고, 프로덕션에서는 적절한 보안 설정을 추가하세요. + +--- + +## 문제 해결 + +### 포트가 이미 사용 중인 경우 + +```bash +# 포트 사용 중인 프로세스 확인 +sudo lsof -i :4200 +# 또는 +sudo netstat -tulpn | grep 4200 + +# 프로세스 종료 +kill -9 +``` + +### 빌드 디렉토리가 없는 경우 + +```bash +# 빌드 실행 +nx build web-manager + +# 빌드 확인 +ls -la dist/apps/web-manager +``` diff --git a/apps/api-server/src/broker/broker.service.ts b/apps/api-server/src/broker/broker.service.ts index 84cc749..d64bb5a 100644 --- a/apps/api-server/src/broker/broker.service.ts +++ b/apps/api-server/src/broker/broker.service.ts @@ -1,5 +1,5 @@ import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; -import { HandleBrokerErrors, checkCmsTokenError } from '@common'; +import { BaseService, HandleBrokerErrors } from '@common'; import { BrokerError } from '@error/broker/broker-error'; import { HostService } from '@host'; import { Injectable } from '@nestjs/common'; @@ -23,26 +23,24 @@ import { * @since 1.0.0 */ @Injectable() -export class BrokerService { +export class BrokerService extends BaseService { constructor( - private readonly hostService: HostService, - private readonly cmsClient: CmsHttpsClientService - ) {} + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService + ) { + super(hostService, cmsClient); + } @HandleBrokerErrors() async getBrokers(userId: string, hostUid: string) { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest = { + const cmsRequest: BaseCmsRequest = { task: 'getbrokersinfo', - token: host.token ? host.token : '', }; - const response = await this.cmsClient.postAuthenticated< - BaseCmsRequest, - GetBrokersInfoCmsResponse - >(url, body); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status !== 'success') { throw BrokerError.GetBrokersFailed(); @@ -52,20 +50,16 @@ export class BrokerService { @HandleBrokerErrors() async stopBroker(userId: string, hostUid: string, bname: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: HandleBrokerCmsRequest = { + const cmsRequest: HandleBrokerCmsRequest = { task: 'broker_stop', - token: host.token ? host.token : '', bname: bname, }; - const response = await this.cmsClient.postAuthenticated< - HandleBrokerCmsRequest, - BaseCmsResponse - >(url, body); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status !== 'success') { throw BrokerError.BrokerStopFailed(); @@ -75,20 +69,16 @@ export class BrokerService { @HandleBrokerErrors() async startBroker(userId: string, hostUid: string, bname: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: HandleBrokerCmsRequest = { + const cmsRequest: HandleBrokerCmsRequest = { task: 'broker_start', - token: host.token ? host.token : '', bname: bname, }; - const response = await this.cmsClient.postAuthenticated< - HandleBrokerCmsRequest, - BaseCmsResponse - >(url, body); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status !== 'success') { throw BrokerError.BrokerStartFailed(); @@ -98,30 +88,27 @@ export class BrokerService { @HandleBrokerErrors() async restartBroker(userId: string, hostUid: string, bname: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; const stopRequest: HandleBrokerCmsRequest = { task: 'broker_stop', - token: host.token ? host.token : '', bname: bname, }; - const response = await this.cmsClient.postAuthenticated< - HandleBrokerCmsRequest, - BaseCmsResponse - >(url, stopRequest); - if (response.status === 'success') { + const stopResponse = await this.executeCmsRequest( + userId, + hostUid, + stopRequest + ); + if (stopResponse.status === 'success') { const startRequest: HandleBrokerCmsRequest = { task: 'broker_start', - token: host.token ? host.token : '', bname: bname, }; - const response = await this.cmsClient.postAuthenticated< + const startResponse = await this.executeCmsRequest< HandleBrokerCmsRequest, BaseCmsResponse - >(url, startRequest); - if (response.status === 'success') { + >(userId, hostUid, startRequest); + if (startResponse.status === 'success') { return true; } else { throw BrokerError.BrokerStartFailed(); @@ -146,24 +133,18 @@ export class BrokerService { hostUid: string, bname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: GetBrokerStatusCmsRequest = { + const cmsRequest: GetBrokerStatusCmsRequest = { task: 'getbrokerstatus', - token: host.token ? host.token : '', bname: bname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< GetBrokerStatusCmsRequest, GetBrokerStatusCmsResponse - >(url, body); - - checkCmsTokenError(response); + >(userId, hostUid, cmsRequest); if (response.status === 'success') { - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - return dataOnly; + return this.extractDomainData(response); } throw BrokerError.GetBrokersFailed({ response }); @@ -171,18 +152,14 @@ export class BrokerService { @HandleBrokerErrors() async stopAllBrokers(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body = { + const cmsRequest: BaseCmsRequest = { task: 'stopbroker', - token: host.token ? host.token : '', }; - const response: BaseCmsResponse = await this.cmsClient.postAuthenticated< - BaseCmsRequest, - BaseCmsResponse - >(url, body); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status === 'success') { return true; @@ -193,18 +170,14 @@ export class BrokerService { @HandleBrokerErrors() async startAllBrokers(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body = { + const cmsRequest: BaseCmsRequest = { task: 'startbroker', - token: host.token ? host.token : '', }; - const response: BaseCmsResponse = await this.cmsClient.postAuthenticated< - BaseCmsRequest, - BaseCmsResponse - >(url, body); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status === 'success') { return true; diff --git a/apps/api-server/src/cms-config/cms-config.service.ts b/apps/api-server/src/cms-config/cms-config.service.ts index 0f10383..77d6471 100644 --- a/apps/api-server/src/cms-config/cms-config.service.ts +++ b/apps/api-server/src/cms-config/cms-config.service.ts @@ -21,7 +21,7 @@ import { GetAllSysParamCmsResponse } from '@type/cms-response/get-all-sys-param- import { ParamdumpCmsResponse } from '@type/cms-response/paramdump-cms-response'; import { StatdumpCmsResponse } from '@type/cms-response/statdump-cms-response'; import { BaseCmsResponse } from '@type/cms-response/base-cms-response'; -import { HandleCmsConfigErrors, checkCmsTokenError, checkCmsStatusError } from '@common'; +import { BaseService, HandleCmsConfigErrors } from '@common'; import { ConfigError } from '@error/config/config-error'; /** @@ -34,11 +34,13 @@ import { ConfigError } from '@error/config/config-error'; * @since 1.0.0 */ @Injectable() -export class CmsConfigService { +export class CmsConfigService extends BaseService { constructor( - private readonly hostService: HostService, - private readonly cmsClient: CmsHttpsClientService - ) {} + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService + ) { + super(hostService, cmsClient); + } /** * Get environment information from a CMS host. @@ -51,30 +53,23 @@ export class CmsConfigService { */ @HandleCmsConfigErrors() async getEnv(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest = { + const cmsRequest: BaseCmsRequest = { task: 'getenv', - token: host.token || '', }; - const response = await this.cmsClient.postAuthenticated( - url, - body + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest ); - checkCmsTokenError(response); - if (response.status === 'success') { - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - return dataOnly; + return this.extractDomainData(response); } - checkCmsStatusError( - response, - `Failed to get environment info: ${response.note || 'Unknown error'}` - ); - throw new Error(`Failed to get environment info: ${response.note || 'Unknown error'}`); + throw ConfigError.GetAllSysParamFailed('getenv', { + note: response.note || 'Unknown error', + }); } /** @@ -93,29 +88,24 @@ export class CmsConfigService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: BaseCmsRequest & { dbname: string; both: 'n' } = { + const cmsRequest: BaseCmsRequest & { dbname: string; both: 'n' } = { task: 'paramdump', - token: host.token || '', both: 'n', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< BaseCmsRequest & { dbname: string; both: 'n' }, ParamdumpCmsResponse - >(url, request); - - checkCmsTokenError(response); + >(userId, hostUid, cmsRequest); if (response.status === 'success') { - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - return dataOnly; + return this.extractDomainData(response); } - checkCmsStatusError(response, `Failed to get paramdump: ${response.note || 'Unknown error'}`); - throw new Error(`Failed to get paramdump: ${response.note || 'Unknown error'}`); + throw ConfigError.GetAllSysParamFailed('paramdump', { + note: response.note || 'Unknown error', + }); } /** @@ -134,29 +124,23 @@ export class CmsConfigService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - - const request: BaseCmsRequest & { dbname: string } = { + const cmsRequest: BaseCmsRequest & { dbname: string } = { task: 'statdump', - token: host.token || '', dbname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< BaseCmsRequest & { dbname: string }, StatdumpCmsResponse - >(url, request); - - checkCmsTokenError(response); + >(userId, hostUid, cmsRequest); if (response.status === 'success') { - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - return dataOnly; + return this.extractDomainData(response); } - checkCmsStatusError(response, `Failed to get statdump: ${response.note || 'Unknown error'}`); - throw new Error(`Failed to get statdump: ${response.note || 'Unknown error'}`); + throw ConfigError.GetAllSysParamFailed('statdump', { + note: response.note || 'Unknown error', + }); } /** @@ -175,30 +159,20 @@ export class CmsConfigService { hostUid: string, confname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: GetAllSysParamCmsRequest = { + const cmsRequest: GetAllSysParamCmsRequest = { task: 'getallsysparam', - token: host.token || '', confname: confname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< GetAllSysParamCmsRequest, GetAllSysParamCmsResponse - >(url, request); - - checkCmsTokenError(response); + >(userId, hostUid, cmsRequest); if (response.status === 'success') { - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - return dataOnly; + return this.extractDomainData(response); } - checkCmsStatusError( - response, - `Failed to get all system parameters: ${response.note || 'Unknown error'}` - ); throw ConfigError.GetAllSysParamFailed(confname, { note: response.note }); } @@ -220,31 +194,19 @@ export class CmsConfigService { confname: string, confdata: string[] ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: SetSysParamCmsRequest = { + const cmsRequest: SetSysParamCmsRequest = { task: 'setsysparam', - token: host.token || '', confname: confname, confdata: confdata, }; - const response = await this.cmsClient.postAuthenticated( - url, - request + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest ); - checkCmsTokenError(response); - - if (response.status === 'success') { - return {}; - } - - checkCmsStatusError( - response, - `Failed to set system parameters: ${response.note || 'Unknown error'}` - ); - throw ConfigError.SetSysParamFailed(confname, { note: response.note }); + return {}; } } diff --git a/apps/api-server/src/common/base.service.ts b/apps/api-server/src/common/base.service.ts new file mode 100644 index 0000000..3a1c91a --- /dev/null +++ b/apps/api-server/src/common/base.service.ts @@ -0,0 +1,72 @@ +import { HostService } from '@host'; +import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; +import { checkCmsStatusError, checkCmsTokenError } from '@common'; +import { Logger } from '@nestjs/common'; +import { BaseCmsRequest } from '@type/cms-request/base-cms-request'; + +/** + * Base service class for CMS API communication. + * Provides common functionality for host lookup, token insertion, and CMS request execution. + * + * @category Business Services + * @since 1.0.0 + */ +export abstract class BaseService { + protected readonly logger: Logger; + + constructor( + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService + ) { + this.logger = new Logger(this.constructor.name); + } + + /** + * Executes a CMS API request with common error handling. + * Handles host lookup, URL construction, authentication, and error checking. + * Automatically adds token to the request. + * + * @param userId User ID from JWT + * @param hostUid Host unique identifier + * @param cmsRequest CMS request object (token will be added automatically) + * @returns CMS response + * @throws Error If host not found, request fails, or CMS status is fail + */ + protected async executeCmsRequest< + TRequest extends Omit, + TResponse + >( + userId: string, + hostUid: string, + cmsRequest: TRequest + ): Promise { + const host = await this.hostService.findHostInternal(userId, hostUid); + const url = `https://${host.address}:${host.port}/cm_api`; + + // Add token to request if not already present + const requestWithToken = { ...cmsRequest, token: host.token || '' }; + + const response = await this.cmsClient.postAuthenticated( + url, + requestWithToken + ); + + checkCmsTokenError(response); + checkCmsStatusError(response); + + return response; + } + + /** + * Extracts domain-only data from CMS response by removing envelope fields. + * + * @param response CMS response with envelope fields + * @returns Response without envelope fields (__EXEC_TIME, note, status, task) + */ + protected extractDomainData( + response: T + ): Omit { + const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + return dataOnly; + } +} diff --git a/apps/api-server/src/common/index.ts b/apps/api-server/src/common/index.ts index 9444c23..6120e1e 100644 --- a/apps/api-server/src/common/index.ts +++ b/apps/api-server/src/common/index.ts @@ -23,3 +23,6 @@ export { HandleCmsConfigErrors } from './decorators/handle-cms-config-errors.dec // Export interceptors export { SuccessResponseInterceptor } from './interceptors/success-response.interceptor'; export { LoggingInterceptor } from './interceptors/logging.interceptor'; + +// Export base service +export { BaseService } from './base.service'; \ No newline at end of file diff --git a/apps/api-server/src/database/backup/database-backup.controller.ts b/apps/api-server/src/database/backup/database-backup.controller.ts index 4981c5d..863fb56 100644 --- a/apps/api-server/src/database/backup/database-backup.controller.ts +++ b/apps/api-server/src/database/backup/database-backup.controller.ts @@ -60,9 +60,8 @@ export class DatabaseBackupController { this.logger ); - Logger.log( - `Adding backup schedule for database: ${dbname} on host: ${hostUid}`, - 'DatabaseBackupController' + this.logger.log( + `Adding backup schedule for database: ${dbname} on host: ${hostUid}` ); return await this.backupService.addBackupSchedule(userId, hostUid, dbname, body); } @@ -97,9 +96,8 @@ export class DatabaseBackupController { this.logger ); - Logger.log( - `Setting backup schedule for database: ${dbname} on host: ${hostUid}`, - 'DatabaseBackupController' + this.logger.log( + `Setting backup schedule for database: ${dbname} on host: ${hostUid}` ); return await this.backupService.setBackupSchedule(userId, hostUid, dbname, body); } @@ -129,9 +127,8 @@ export class DatabaseBackupController { validateRequiredFields(body, ['backupid'], 'database/backup-schedule', this.logger); - Logger.log( - `Deleting backup schedule for database: ${dbname} on host: ${hostUid}`, - 'DatabaseBackupController' + this.logger.log( + `Deleting backup schedule for database: ${dbname} on host: ${hostUid}` ); return await this.backupService.deleteBackupSchedule(userId, hostUid, dbname, body); } @@ -156,9 +153,8 @@ export class DatabaseBackupController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting backup schedule for database: ${dbname} on host: ${hostUid}`, - 'DatabaseBackupController' + this.logger.log( + `Getting backup schedule for database: ${dbname} on host: ${hostUid}` ); return await this.backupService.getBackupSchedule(userId, hostUid, dbname); } @@ -184,9 +180,8 @@ export class DatabaseBackupController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting auto-backup database error log on host: ${hostUid}`, - 'DatabaseBackupController' + this.logger.log( + `Getting auto-backup database error log on host: ${hostUid}` ); return await this.backupService.getAutoBackupDbErrLog(userId, hostUid, body); } diff --git a/apps/api-server/src/database/backup/database-backup.service.ts b/apps/api-server/src/database/backup/database-backup.service.ts index ddccf19..66a652d 100644 --- a/apps/api-server/src/database/backup/database-backup.service.ts +++ b/apps/api-server/src/database/backup/database-backup.service.ts @@ -11,13 +11,12 @@ import { } from '@api-interfaces'; import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; import { - checkCmsStatusError, - checkCmsTokenError, + BaseService, HandleDatabaseErrors, } from '@common'; import { DatabaseError } from '@error/database/database-error'; import { HostService } from '@host'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { AddBackupInfoCmsRequest, DeleteBackupInfoCmsRequest, @@ -31,6 +30,7 @@ import { GetBackupInfoCmsResponse, SetBackupInfoCmsResponse, GetAutoBackupDbErrLogCmsResponse, + BackupInfo, } from '@type/cms-response'; /** @@ -41,13 +41,13 @@ import { * @since 1.0.0 */ @Injectable() -export class DatabaseBackupService { - private readonly logger = new Logger(DatabaseBackupService.name); - +export class DatabaseBackupService extends BaseService { constructor( - private readonly hostService: HostService, - private readonly cmsClient: CmsHttpsClientService - ) {} + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService + ) { + super(hostService, cmsClient); + } /** * Add automated backup schedule information for a database. @@ -67,11 +67,8 @@ export class DatabaseBackupService { dbname: string, backupInfo: AddBackupInfoClientRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: AddBackupInfoCmsRequest = { + const cmsRequest: AddBackupInfoCmsRequest = { task: 'addbackupinfo', - token: host.token || '', dbname: dbname, backupid: backupInfo.backupid, path: backupInfo.path, @@ -89,14 +86,11 @@ export class DatabaseBackupService { bknum: backupInfo.bknum, }; - const response = await this.cmsClient.postAuthenticated< - AddBackupInfoCmsRequest, - AddBackupInfoCmsResponse - >(url, request); - - checkCmsTokenError(response); - - checkCmsStatusError(response); + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); return {}; } @@ -119,11 +113,8 @@ export class DatabaseBackupService { dbname: string, backupInfo: SetBackupInfoClientRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: SetBackupInfoCmsRequest = { + const cmsRequest: SetBackupInfoCmsRequest = { task: 'setbackupinfo', - token: host.token || '', dbname: dbname, backupid: backupInfo.backupid, path: backupInfo.path, @@ -141,14 +132,10 @@ export class DatabaseBackupService { bknum: backupInfo.bknum, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< SetBackupInfoCmsRequest, SetBackupInfoCmsResponse - >(url, request); - - checkCmsTokenError(response); - - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); return { __EXEC_TIME: response.__EXEC_TIME, @@ -176,23 +163,16 @@ export class DatabaseBackupService { dbname: string, backupInfo: DeleteBackupInfoClientRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: DeleteBackupInfoCmsRequest = { + const cmsRequest: DeleteBackupInfoCmsRequest = { task: 'deletebackupinfo', - token: host.token || '', dbname: dbname, backupid: backupInfo.backupid, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< DeleteBackupInfoCmsRequest, DeleteBackupInfoCmsResponse - >(url, request); - - checkCmsTokenError(response); - - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); return { __EXEC_TIME: response.__EXEC_TIME, @@ -218,25 +198,23 @@ export class DatabaseBackupService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: GetBackupInfoCmsRequest = { + const cmsRequest: GetBackupInfoCmsRequest = { task: 'getbackupinfo', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< + this.logger.debug(`Getting backup schedule information for database: ${dbname}`); + + const response = await this.executeCmsRequest< GetBackupInfoCmsRequest, GetBackupInfoCmsResponse - >(url, request); - - checkCmsTokenError(response); + >(userId, hostUid, cmsRequest); - checkCmsStatusError(response); - - const { __EXEC_TIME, note, status, task, dbname: responseDbname, ...rest } = response; - const backupArray = rest[dbname] as any[]; + // Extract dbname before extractDomainData to preserve string type + const responseDbname = response.dbname as string; + const { dbname: _, ...rest } = this.extractDomainData(response); + // CMS API returns backup info with database name as a dynamic key + const backupArray = (rest[dbname] as BackupInfo[] | undefined) || []; return { dbname: responseDbname, @@ -260,24 +238,17 @@ export class DatabaseBackupService { hostUid: string, request: GetAutoBackupDbErrLogRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: GetAutoBackupDbErrLogCmsRequest = { task: 'getautobackupdberrlog', - token: host.token || '', }; - const response = await this.cmsClient.postAuthenticated< + this.logger.debug('Getting auto-backup database error log'); + + const response = await this.executeCmsRequest< GetAutoBackupDbErrLogCmsRequest, GetAutoBackupDbErrLogCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); - - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + >(userId, hostUid, cmsRequest); - return dataOnly; + return this.extractDomainData(response); } } diff --git a/apps/api-server/src/database/base-database.service.ts b/apps/api-server/src/database/base-database.service.ts new file mode 100644 index 0000000..6c04b7b --- /dev/null +++ b/apps/api-server/src/database/base-database.service.ts @@ -0,0 +1,5 @@ +/** + * @deprecated Use BaseService from '@common' instead. + * This file is kept for backward compatibility but will be removed in a future version. + */ +export { BaseService as BaseDatabaseService } from '@common'; diff --git a/apps/api-server/src/database/config/database-config.controller.ts b/apps/api-server/src/database/config/database-config.controller.ts index f94bbe2..abe0aaa 100644 --- a/apps/api-server/src/database/config/database-config.controller.ts +++ b/apps/api-server/src/database/config/database-config.controller.ts @@ -59,9 +59,8 @@ export class DatabaseConfigController { validateRequiredFields(body, ['planlist'], 'database/auto-exec-query', this.logger); - Logger.log( - `Setting auto-exec query for database: ${dbname} on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Setting auto-exec query for database: ${dbname} on host: ${hostUid}` ); return await this.configService.setAutoExecQuery(userId, hostUid, dbname, body); } @@ -86,9 +85,8 @@ export class DatabaseConfigController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting auto-exec query for database: ${dbname} on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Getting auto-exec query for database: ${dbname} on host: ${hostUid}` ); return await this.configService.getAutoExecQuery(userId, hostUid, dbname); } @@ -114,9 +112,8 @@ export class DatabaseConfigController { ): Promise { const userId = req.user.sub; - Logger.log( - `Enabling auto-start for database: ${body.dbname} on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Enabling auto-start for database: ${body.dbname} on host: ${hostUid}` ); return await this.configService.setAutoStart(userId, hostUid, body); } @@ -142,9 +139,8 @@ export class DatabaseConfigController { ): Promise { const userId = req.user.sub; - Logger.log( - `Disabling auto-start for database: ${body.dbname} on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Disabling auto-start for database: ${body.dbname} on host: ${hostUid}` ); return await this.configService.removeAutoStart(userId, hostUid, body); } @@ -186,9 +182,8 @@ export class DatabaseConfigController { this.logger ); - Logger.log( - `Setting auto-add volume for database: ${dbname} on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Setting auto-add volume for database: ${dbname} on host: ${hostUid}` ); return await this.configService.setAutoAddVol(userId, hostUid, dbname, body); } @@ -218,9 +213,8 @@ export class DatabaseConfigController { validateRequiredFields(body, ['dbstatus'], 'database/class-info', this.logger); - Logger.log( - `Getting class info for database: ${dbname} on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Getting class info for database: ${dbname} on host: ${hostUid}` ); return await this.configService.getClassInfo(userId, hostUid, dbname, body); } @@ -246,9 +240,8 @@ export class DatabaseConfigController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting auto-exec query error log on host: ${hostUid}`, - 'DatabaseConfigController' + this.logger.log( + `Getting auto-exec query error log on host: ${hostUid}` ); return await this.configService.getAutoExecQueryErrLog(userId, hostUid, body); } diff --git a/apps/api-server/src/database/config/database-config.service.ts b/apps/api-server/src/database/config/database-config.service.ts index bc7e9e6..729904b 100644 --- a/apps/api-server/src/database/config/database-config.service.ts +++ b/apps/api-server/src/database/config/database-config.service.ts @@ -16,6 +16,7 @@ import { import { CmsConfigService } from '@cms-config/cms-config.service'; import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; import { + BaseService, checkCmsStatusError, checkCmsTokenError, HandleCmsConfigErrors, @@ -24,7 +25,7 @@ import { import { ConfigError } from '@error/config/config-error'; import { DatabaseError } from '@error/database/database-error'; import { HostService } from '@host'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { GetAutoExecQueryCmsRequest, SetAutoExecQueryCmsRequest, @@ -41,6 +42,7 @@ import { } from '@type/cms-response'; import { GetAllSysParamCmsResponse } from '@type/cms-response/get-all-sys-param-cms-response'; import { parseConfigParams } from '@util'; +import { DATABASE_CONSTANTS } from '../database.constants'; /** * Service for managing database configuration operations. @@ -50,14 +52,14 @@ import { parseConfigParams } from '@util'; * @since 1.0.0 */ @Injectable() -export class DatabaseConfigService { - private readonly logger = new Logger(DatabaseConfigService.name); - +export class DatabaseConfigService extends BaseService { constructor( - private readonly hostService: HostService, - private readonly cmsClient: CmsHttpsClientService, + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService, private readonly cmsConfigService: CmsConfigService - ) {} + ) { + super(hostService, cmsClient); + } /** * Set auto-execution query for a database. @@ -77,23 +79,17 @@ export class DatabaseConfigService { dbname: string, autoExecQuery: SetAutoExecQueryClientRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: SetAutoExecQueryCmsRequest = { + const cmsRequest: SetAutoExecQueryCmsRequest = { task: 'setautoexecquery', - token: host.token || '', dbname: dbname, planlist: autoExecQuery.planlist, }; - const response = await this.cmsClient.postAuthenticated< - SetAutoExecQueryCmsRequest, - SetAutoExecQueryCmsResponse - >(url, request); - - checkCmsTokenError(response); - - checkCmsStatusError(response); + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); return {}; } @@ -114,38 +110,35 @@ export class DatabaseConfigService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: GetAutoExecQueryCmsRequest = { + const cmsRequest: GetAutoExecQueryCmsRequest = { task: 'getautoexecquery', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< GetAutoExecQueryCmsRequest, GetAutoExecQueryCmsResponse - >(url, request); - - checkCmsTokenError(response); - - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + const dataOnly = this.extractDomainData(response); const planlist = dataOnly.planlist.map((plan) => { const queryplan = plan.queryplan.map((query) => { - const queryAny = query as any; - - if (queryAny['@username'] !== undefined) { - const { '@username': atUsername, ...rest } = queryAny; + // Handle @username field (from XML) and convert to username + // Client response requires username to be non-optional + if ('@username' in query && query['@username'] !== undefined) { + const { '@username': atUsername, ...rest } = query; return { ...rest, username: atUsername || '', }; } - return queryAny; + // Ensure username exists (required in client response) + return { + ...query, + username: query.username || '', + }; }); return { @@ -161,13 +154,15 @@ export class DatabaseConfigService { /** * Enable auto-start for a database. - * Adds database name to the server parameter in configuration file. + * Adds database name to the server parameter in cubridconf configuration file. + * If server parameter does not exist, it will be added to the [service] section. + * The confname field in the request is ignored - the system always uses 'cubridconf'. * * @param userId User ID from JWT * @param hostUid Host UID - * @param request Request containing confname and dbname - * @returns SetAutoStartResponse Empty object on success - * @throws DatabaseError If request fails, server parameter not found, or dbname already exists + * @param request Request containing dbname (confname is ignored, always uses 'cubridconf') + * @returns SetAutoStartResponse Configuration response on success + * @throws ConfigError If request fails, [service] section not found, or server parameter cannot be added */ @HandleCmsConfigErrors() async setAutoStart( @@ -175,72 +170,114 @@ export class DatabaseConfigService { hostUid: string, request: SetAutoStartRequest ): Promise { - // Get current configuration + const confname = DATABASE_CONSTANTS.CUBRID_CONF_NAME; + + // Get current configuration from cubridconf const currentConfig = await this.cmsConfigService.getAllSystemParam( userId, hostUid, - request.confname + confname ); if (!currentConfig.conflist || currentConfig.conflist.length === 0) { - throw ConfigError.NoConflistData(request.confname); + throw ConfigError.NoConflistData(confname); } const confdata = currentConfig.conflist[0].confdata; if (!confdata || confdata.length === 0) { - throw ConfigError.NoConfdata(request.confname); + throw ConfigError.NoConfdata(confname); } - this.logger.debug(JSON.stringify((currentConfig))); + this.logger.debug(JSON.stringify(currentConfig)); - // Find the server parameter using utility function - // Type assertion is safe because parseConfigParams only uses conflist property + // Find the server parameter in the [service] section const params = parseConfigParams(currentConfig as GetAllSysParamCmsResponse); - const serverParam = params.find((param) => param.key === 'server'); + const serverParam = params.find( + (param) => param.key === 'server' && param.section === 'service' + ); + + let updatedConfdata: string[]; if (!serverParam) { - throw ConfigError.ServerParamNotFound(request.confname); - } + // Server parameter does not exist in [service] section + // Find [service] section in confdata (case-sensitive, lowercase) + let serviceSectionStartIndex = -1; - // Parse existing server values - const existingDbnames = serverParam.value - ? serverParam.value - .split(',') - .map((db) => db.trim()) - .filter((db) => db.length > 0) - : []; + for (let i = 0; i < confdata.length; i++) { + const line = confdata[i].trim(); - // Check if dbname already exists - if (existingDbnames.includes(request.dbname)) { - throw ConfigError.DbnameAlreadyExists(request.confname, request.dbname); + // Check for [service] section (case-sensitive, lowercase) + if (line === '[service]') { + serviceSectionStartIndex = i; + break; + } + } + + if (serviceSectionStartIndex === -1) { + // [service] section does not exist, throw error + throw ConfigError.ServerParamNotFound(confname, { + message: '[service] section not found in configuration file', + }); + } + + // [service] section exists, add server parameter after the section header + const insertIndex = serviceSectionStartIndex + 1; + updatedConfdata = [ + ...confdata.slice(0, insertIndex), + `server=${request.dbname}`, + ...confdata.slice(insertIndex), + ]; + this.logger.debug( + `Server parameter not found in [service] section, adding server=${request.dbname} at line ${insertIndex + 1}` + ); + } else { + // Parse existing server values + const existingDbnames = serverParam.value + ? serverParam.value + .split(',') + .map((db) => db.trim()) + .filter((db) => db.length > 0) + : []; + + // Check if dbname already exists - if it does, return success without modification + if (existingDbnames.includes(request.dbname)) { + this.logger.debug( + `Database name ${request.dbname} already exists in server parameter, returning current configuration` + ); + const rv: SetAutoStartResponse = currentConfig as unknown as SetAutoStartResponse; + this.logger.debug(JSON.stringify(rv)); + return rv; + } + + // Append new dbname + const updatedDbnames = [...existingDbnames, request.dbname]; + const updatedServerLine = `server=${updatedDbnames.join(',')}`; + + // Update confdata with new server line (lineNumber is 1-based, convert to 0-based index) + updatedConfdata = [...confdata]; + updatedConfdata[serverParam.lineNumber - 1] = updatedServerLine; } - // Append new dbname - const updatedDbnames = [...existingDbnames, request.dbname]; - const updatedServerLine = `server=${updatedDbnames.join(',')}`; - - // Update confdata with new server line (lineNumber is 1-based, convert to 0-based index) - const updatedConfdata = [...confdata]; - updatedConfdata[serverParam.lineNumber - 1] = updatedServerLine; const rv = await this.cmsConfigService.setSystemParam( userId, hostUid, - request.confname, + confname, updatedConfdata ); // Set updated configuration this.logger.debug(JSON.stringify(rv)); - return rv + return rv; } /** * Disable auto-start for a database. - * Removes database name from the server parameter in configuration file. + * Removes database name from the server parameter in cubridconf configuration file. + * The confname field in the request is ignored - the system always uses 'cubridconf'. * * @param userId User ID from JWT * @param hostUid Host UID - * @param request Request containing confname and dbname + * @param request Request containing dbname (confname is ignored, always uses 'cubridconf') * @returns RemoveAutoStartResponse Empty object on success - * @throws DatabaseError If request fails, server parameter not found, or dbname does not exist + * @throws ConfigError If request fails, server parameter not found in [service] section, or dbname does not exist */ @HandleCmsConfigErrors() async removeAutoStart( @@ -248,29 +285,34 @@ export class DatabaseConfigService { hostUid: string, request: RemoveAutoStartRequest ): Promise { - // Get current configuration + const confname = DATABASE_CONSTANTS.CUBRID_CONF_NAME; + + // Get current configuration from cubridconf const currentConfig = await this.cmsConfigService.getAllSystemParam( userId, hostUid, - request.confname + confname ); if (!currentConfig.conflist || currentConfig.conflist.length === 0) { - throw ConfigError.NoConflistData(request.confname); + throw ConfigError.NoConflistData(confname); } const confdata = currentConfig.conflist[0].confdata; if (!confdata || confdata.length === 0) { - throw ConfigError.NoConfdata(request.confname); + throw ConfigError.NoConfdata(confname); } - // Find the server parameter using utility function - // Type assertion is safe because parseConfigParams only uses conflist property + // Find the server parameter in the [service] section const params = parseConfigParams(currentConfig as GetAllSysParamCmsResponse); - const serverParam = params.find((param) => param.key === 'server'); + const serverParam = params.find( + (param) => param.key === 'server' && param.section === 'service' + ); if (!serverParam) { - throw ConfigError.ServerParamNotFound(request.confname); + throw ConfigError.ServerParamNotFound(confname, { + message: 'server parameter not found in [service] section', + }); } // Parse existing server values @@ -283,7 +325,7 @@ export class DatabaseConfigService { // Check if dbname exists if (!existingDbnames.includes(request.dbname)) { - throw ConfigError.DbnameNotFound(request.confname, request.dbname); + throw ConfigError.DbnameNotFound(confname, request.dbname); } // Remove dbname @@ -299,7 +341,7 @@ export class DatabaseConfigService { return await this.cmsConfigService.setSystemParam( userId, hostUid, - request.confname, + confname, updatedConfdata ); } @@ -322,11 +364,8 @@ export class DatabaseConfigService { dbname: string, request: SetAutoAddVolRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; const cmsRequest: SetAutoAddVolCmsRequest = { task: 'setautoaddvol', - token: host.token || '', dbname: dbname, data: request.data, data_warn_outofspace: request.data_warn_outofspace, @@ -336,14 +375,11 @@ export class DatabaseConfigService { index_ext_page: request.index_ext_page, }; - const response = await this.cmsClient.postAuthenticated< - SetAutoAddVolCmsRequest, - SetAutoAddVolCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - - checkCmsStatusError(response); + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); return {}; } @@ -366,27 +402,19 @@ export class DatabaseConfigService { dbname: string, request: ClassInfoRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: ClassInfoCmsRequest = { task: 'classinfo', - token: host.token || '', dbname: dbname, dbstatus: request.dbstatus, }; - const response = await this.cmsClient.postAuthenticated< - ClassInfoCmsRequest, - ClassInfoCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); - - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); - return dataOnly; + return this.extractDomainData(response); } /** @@ -405,24 +433,17 @@ export class DatabaseConfigService { hostUid: string, request: GetAutoExecQueryErrLogRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: GetAutoExecQueryErrLogCmsRequest = { task: 'getautoexecqueryerrlog', - token: host.token || '', }; - const response = await this.cmsClient.postAuthenticated< + this.logger.debug('Getting auto-execution query error log'); + + const response = await this.executeCmsRequest< GetAutoExecQueryErrLogCmsRequest, GetAutoExecQueryErrLogCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); - - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + >(userId, hostUid, cmsRequest); - return dataOnly; + return this.extractDomainData(response); } } diff --git a/apps/api-server/src/database/database.constants.ts b/apps/api-server/src/database/database.constants.ts new file mode 100644 index 0000000..91241d8 --- /dev/null +++ b/apps/api-server/src/database/database.constants.ts @@ -0,0 +1,22 @@ +/** + * Constants used across database module. + * + * @category Database + * @since 1.0.0 + */ +export const DATABASE_CONSTANTS = { + /** + * CUBRID configuration file name + */ + CUBRID_CONF_NAME: 'cubridconf', + + /** + * CMS API protocol + */ + CMS_API_PROTOCOL: 'https://', + + /** + * CMS API path + */ + CMS_API_PATH: '/cm_api', +} as const; diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts index f3e9707..f163da2 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts @@ -46,7 +46,7 @@ export class DatabaseLifecycleController { ): Promise { const userId = req.user.sub; - Logger.log(`Getting start info for host: ${hostUid}`, 'DatabaseLifecycleController'); + this.logger.log(`Getting start info for host: ${hostUid}`); const response = await this.lifecycleService.startInfo(userId, hostUid); return response; } @@ -69,7 +69,7 @@ export class DatabaseLifecycleController { ): Promise { const userId = req.user.sub; - Logger.log(`Getting create info for host: ${hostUid}`, 'DatabaseLifecycleController'); + this.logger.log(`Getting create info for host: ${hostUid}`); return await this.lifecycleService.getCreatedbInfo(userId, hostUid); } @@ -93,7 +93,7 @@ export class DatabaseLifecycleController { ): Promise { const userId = req.user.sub; - Logger.log(`Starting database: ${dbname} on host: ${hostUid}`, 'DatabaseLifecycleController'); + this.logger.log(`Starting database: ${dbname} on host: ${hostUid}`); const result = await this.lifecycleService.startDatabase(userId, hostUid, dbname); return result; } @@ -118,7 +118,7 @@ export class DatabaseLifecycleController { ): Promise { const userId = req.user.sub; - Logger.log(`Stopping database: ${dbname} on host: ${hostUid}`, 'DatabaseLifecycleController'); + this.logger.log(`Stopping database: ${dbname} on host: ${hostUid}`); const result = await this.lifecycleService.stopDatabase(userId, hostUid, dbname); return result; } @@ -143,7 +143,7 @@ export class DatabaseLifecycleController { ): Promise { const userId = req.user.sub; - Logger.log(`Restarting database: ${dbname} on host: ${hostUid}`, 'DatabaseLifecycleController'); + this.logger.log(`Restarting database: ${dbname} on host: ${hostUid}`); const result = await this.lifecycleService.restartDatabase(userId, hostUid, dbname); return result; } @@ -226,7 +226,7 @@ export class DatabaseLifecycleController { this.logger ); - Logger.log(`Creating database: ${body.dbname} on host: ${hostUid}`, 'DatabaseLifecycleController'); + this.logger.log(`Creating database: ${body.dbname} on host: ${hostUid}`); return await this.lifecycleService.createDatabase(userId, hostUid, body); } @@ -250,9 +250,8 @@ export class DatabaseLifecycleController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting volume info for database: ${dbname} on host: ${hostUid}`, - 'DatabaseLifecycleController' + this.logger.log( + `Getting volume info for database: ${dbname} on host: ${hostUid}` ); const response = await this.lifecycleService.getDBSpaceInfo(userId, hostUid, dbname); return response; diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts index 64894e2..0e2177c 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts @@ -10,8 +10,7 @@ import { GetCreatedbInfoClientResponse } from '@api-interfaces/response/get-crea import { CmsConfigService } from '@cms-config/cms-config.service'; import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; import { - checkCmsStatusError, - checkCmsTokenError, + BaseService, HandleDatabaseErrors, } from '@common'; import { DatabaseError } from '@error/database/database-error'; @@ -19,11 +18,12 @@ import { HostError } from '@error/index'; import { ValidationError } from '@error/validation/validation-error'; import { FileService } from '@file/file.service'; import { HostService } from '@host'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { UserRepositoryService } from '@repository'; import { BaseCmsRequest, BaseCmsResponse } from '@type'; import { DatabaseUserService } from '../user/database-user.service'; import { DatabaseConfigService } from '../config/database-config.service'; +import { DATABASE_CONSTANTS } from '../database.constants'; import { CreateDatabaseCmsRequest, DbSpaceInfoCmsRequest, @@ -45,18 +45,18 @@ import { convertExvolArrayToCmsFormat } from '@util'; * @since 1.0.0 */ @Injectable() -export class DatabaseLifecycleService { - private readonly logger = new Logger(DatabaseLifecycleService.name); - +export class DatabaseLifecycleService extends BaseService { constructor( - private readonly hostService: HostService, - private readonly cmsClient: CmsHttpsClientService, + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService, private readonly repository: UserRepositoryService, private readonly cmsConfigService: CmsConfigService, private readonly fileService: FileService, private readonly databaseUserService: DatabaseUserService, private readonly databaseConfigService: DatabaseConfigService - ) {} + ) { + super(hostService, cmsClient); + } /** * Get start information for databases on a host (internal use). @@ -70,19 +70,13 @@ export class DatabaseLifecycleService { */ @HandleDatabaseErrors() async startInfoInternal(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - - const url = `https://${host.address}:${host.port}/cm_api`; - const data: BaseCmsRequest = { + const cmsRequest: BaseCmsRequest = { task: 'startinfo', - token: host.token || '', }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< BaseCmsRequest, StartInfoCmsResponse | BaseCmsResponse - >(url, data); - - checkCmsTokenError(response); + >(userId, hostUid, cmsRequest); if (response.status === 'success') { return response as StartInfoCmsResponse; @@ -104,7 +98,7 @@ export class DatabaseLifecycleService { async startInfo(userId: string, hostUid: string): Promise { const host = await this.hostService.findHostInternal(userId, hostUid); const response = await this.startInfoInternal(userId, hostUid); - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + const dataOnly = this.extractDomainData(response); const dbProfiles = host.dbProfiles || {}; const dbs = dataOnly.dblist?.[0]?.dbs || []; const activeList = dataOnly.activelist?.[0]?.active || []; @@ -137,20 +131,16 @@ export class DatabaseLifecycleService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const data: StartDatabaseCmsRequest = { + const cmsRequest: StartDatabaseCmsRequest = { task: 'startdb', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< - StartDatabaseCmsRequest, - BaseCmsResponse - >(url, data); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status === 'success') { return await this.startInfo(userId, hostUid); @@ -174,20 +164,16 @@ export class DatabaseLifecycleService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const data: StopDatabaseCmsRequest = { + const cmsRequest: StopDatabaseCmsRequest = { task: 'stopdb', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< - StopDatabaseCmsRequest, - BaseCmsResponse - >(url, data); - - checkCmsTokenError(response); + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); if (response.status === 'success') { return await this.startInfo(userId, hostUid); @@ -211,35 +197,27 @@ export class DatabaseLifecycleService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const stopRequest: StopDatabaseCmsRequest = { task: 'stopdb', - token: host.token || '', dbname: dbname, }; - const stopResponse = await this.cmsClient.postAuthenticated< - StopDatabaseCmsRequest, - BaseCmsResponse - >(url, stopRequest); - - checkCmsTokenError(stopResponse); + const stopResponse = await this.executeCmsRequest( + userId, + hostUid, + stopRequest + ); if (stopResponse.status === 'success') { const startRequest: StartDatabaseCmsRequest = { task: 'startdb', - token: host.token || '', dbname: dbname, }; - const startResponse = await this.cmsClient.postAuthenticated< + const startResponse = await this.executeCmsRequest< StartDatabaseCmsRequest, BaseCmsResponse - >(url, startRequest); - - checkCmsTokenError(startResponse); + >(userId, hostUid, startRequest); if (startResponse.status === 'success') { return await this.startInfo(userId, hostUid); @@ -331,18 +309,14 @@ export class DatabaseLifecycleService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const startInfoRequest: BaseCmsRequest = { task: 'startinfo', - token: host.token || '', }; - const startInfo = await this.cmsClient.postAuthenticated< + const startInfo = await this.executeCmsRequest< BaseCmsRequest, StartInfoCmsResponse | BaseCmsResponse - >(url, startInfoRequest); + >(userId, hostUid, startInfoRequest); if ('dblist' in startInfo && 'activelist' in startInfo) { const dbExists = startInfo.dblist.some((el) => el.dbs.some((db) => db.dbname === dbname)); @@ -351,25 +325,20 @@ export class DatabaseLifecycleService { throw DatabaseError.NoSuchDatabase({ dbname, hostUid }); } } else { - checkCmsTokenError(startInfo); throw DatabaseError.InternalError(); } const spaceInfoRequest: DbSpaceInfoCmsRequest = { task: 'dbspaceinfo', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< DbSpaceInfoCmsRequest, DbSpaceInfoCmsResponse | BaseCmsResponse - >(url, spaceInfoRequest); - - checkCmsTokenError(response); + >(userId, hostUid, spaceInfoRequest); if (response.status === 'success') { - const { __EXEC_TIME, note, status, task, ...dataOnly } = response as DbSpaceInfoCmsResponse; - return dataOnly; + return this.extractDomainData(response as DbSpaceInfoCmsResponse); } else { throw DatabaseError.GetDBSpaceInfoFailed({ response, dbname }); } @@ -413,7 +382,6 @@ export class DatabaseLifecycleService { request: CreateDatabaseClientRequest ): Promise { const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; // Collect files to check before parsing exvol const filesToCheck: string[] = []; @@ -450,7 +418,6 @@ export class DatabaseLifecycleService { // Convert numeric values to strings as CMS expects string format const cmsRequest: CreateDatabaseCmsRequest = { task: 'createdb', - token: host.token || '', dbname: request.dbname, numpage: String(request.numpage), pagesize: String(request.pagesize), @@ -463,13 +430,11 @@ export class DatabaseLifecycleService { overwrite_config_file: request.overwrite_config_file, }; - const response = await this.cmsClient.postAuthenticated< - CreateDatabaseCmsRequest, - CreateDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); return {}; } @@ -515,25 +480,33 @@ export class DatabaseLifecycleService { success: true, data: startInfo, }; - } catch (error: any) { - this.logger.error(`Failed to start database: ${error.message}`, error.stack); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = (error as any)?.code || (error instanceof Error ? error.name : 'UNKNOWN'); + const errorDetails = (error as any)?.details; + this.logger.error(`Failed to start database: ${errorMessage}`, errorStack); response.startDatabase = { success: false, error: { - message: error.message || 'Failed to start database', - code: error.code || error.name, - details: error.details, + message: errorMessage || 'Failed to start database', + code: errorCode, + details: errorDetails, }, }; } - } catch (error: any) { - this.logger.error(`Failed to create database: ${error.message}`, error.stack); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = (error as any)?.code || (error instanceof Error ? error.name : 'UNKNOWN'); + const errorDetails = (error as any)?.details; + this.logger.error(`Failed to create database: ${errorMessage}`, errorStack); response.createDatabase = { success: false, error: { - message: error.message || 'Failed to create database', - code: error.code || error.name, - details: error.details, + message: errorMessage || 'Failed to create database', + code: errorCode, + details: errorDetails, }, }; } @@ -557,14 +530,18 @@ export class DatabaseLifecycleService { success: true, data: updateUserResult, }; - } catch (error: any) { - this.logger.error(`Failed to update user: ${error.message}`, error.stack); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = (error as any)?.code || (error instanceof Error ? error.name : 'UNKNOWN'); + const errorDetails = (error as any)?.details; + this.logger.error(`Failed to update user: ${errorMessage}`, errorStack); response.updateUser = { success: false, error: { - message: error.message || 'Failed to update user', - code: error.code || error.name, - details: error.details, + message: errorMessage || 'Failed to update user', + code: errorCode, + details: errorDetails, }, }; } @@ -583,14 +560,18 @@ export class DatabaseLifecycleService { success: true, data: setAutoAddVolResult, }; - } catch (error: any) { - this.logger.error(`Failed to set auto-add volume: ${error.message}`, error.stack); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = (error as any)?.code || (error instanceof Error ? error.name : 'UNKNOWN'); + const errorDetails = (error as any)?.details; + this.logger.error(`Failed to set auto-add volume: ${errorMessage}`, errorStack); response.setAutoAddVol = { success: false, error: { - message: error.message || 'Failed to set auto-add volume', - code: error.code || error.name, - details: error.details, + message: errorMessage || 'Failed to set auto-add volume', + code: errorCode, + details: errorDetails, }, }; } @@ -601,21 +582,25 @@ export class DatabaseLifecycleService { try { // Use top-level dbname and automatically use "cubridconf" as confname const setAutoStartResult = await this.databaseConfigService.setAutoStart(userId, hostUid, { - confname: 'cubridconf', + confname: DATABASE_CONSTANTS.CUBRID_CONF_NAME, dbname: createDbRequest.dbname, }); response.setAutoStart = { success: true, data: setAutoStartResult, }; - } catch (error: any) { - this.logger.error(`Failed to set auto-start: ${error.message}`, error.stack); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = (error as any)?.code || (error instanceof Error ? error.name : 'UNKNOWN'); + const errorDetails = (error as any)?.details; + this.logger.error(`Failed to set auto-start: ${errorMessage}`, errorStack); response.setAutoStart = { success: false, error: { - message: error.message || 'Failed to set auto-start', - code: error.code || error.name, - details: error.details, + message: errorMessage || 'Failed to set auto-start', + code: errorCode, + details: errorDetails, }, }; } diff --git a/apps/api-server/src/database/management/database-management.controller.ts b/apps/api-server/src/database/management/database-management.controller.ts index 0944bdc..bec38ef 100644 --- a/apps/api-server/src/database/management/database-management.controller.ts +++ b/apps/api-server/src/database/management/database-management.controller.ts @@ -74,7 +74,7 @@ export class DatabaseManagementController { this.logger ); - Logger.log(`Unloading database: ${dbname} on host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Unloading database: ${dbname} on host: ${hostUid}`); return await this.managementService.unloadDatabase(userId, hostUid, dbname, body); } @@ -96,7 +96,7 @@ export class DatabaseManagementController { ): Promise { const userId = req.user.sub; - Logger.log(`Getting unload info for host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Getting unload info for host: ${hostUid}`); return await this.managementService.getUnloadInfo(userId, hostUid); } @@ -143,7 +143,7 @@ export class DatabaseManagementController { this.logger ); - Logger.log(`Loading database: ${dbname} on host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Loading database: ${dbname} on host: ${hostUid}`); return await this.managementService.loadDatabase(userId, hostUid, dbname, body); } @@ -170,7 +170,7 @@ export class DatabaseManagementController { ): Promise { const userId = req.user.sub; - Logger.log(`Optimizing database: ${dbname} on host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Optimizing database: ${dbname} on host: ${hostUid}`); return await this.managementService.optimizeDatabase(userId, hostUid, dbname, body); } @@ -199,7 +199,7 @@ export class DatabaseManagementController { validateRequiredFields(body, ['repairdb'], 'database/check', this.logger); - Logger.log(`Checking database: ${dbname} on host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Checking database: ${dbname} on host: ${hostUid}`); return await this.managementService.checkDatabase(userId, hostUid, dbname, body); } @@ -228,7 +228,7 @@ export class DatabaseManagementController { validateRequiredFields(body, ['verbose'], 'database/compact', this.logger); - Logger.log(`Compacting database: ${dbname} on host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Compacting database: ${dbname} on host: ${hostUid}`); return await this.managementService.compactDatabase(userId, hostUid, dbname, body); } @@ -270,7 +270,7 @@ export class DatabaseManagementController { }); } - Logger.log(`Renaming database: ${dbname} to ${body.rename} on host: ${hostUid}`, 'DatabaseManagementController'); + this.logger.log(`Renaming database: ${dbname} to ${body.rename} on host: ${hostUid}`); return await this.managementService.renameDatabase(userId, hostUid, dbname, body); } @@ -294,9 +294,8 @@ export class DatabaseManagementController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting add vol status for database: ${dbname} on host: ${hostUid}`, - 'DatabaseManagementController' + this.logger.log( + `Getting add vol status for database: ${dbname} on host: ${hostUid}` ); return await this.managementService.getAddVolStatus(userId, hostUid, dbname); } @@ -331,9 +330,8 @@ export class DatabaseManagementController { this.logger ); - Logger.log( - `Adding volume to database: ${dbname} on host: ${hostUid}`, - 'DatabaseManagementController' + this.logger.log( + `Adding volume to database: ${dbname} on host: ${hostUid}` ); return await this.managementService.addVolDb(userId, hostUid, dbname, body); } @@ -361,9 +359,8 @@ export class DatabaseManagementController { ): Promise { const userId = req.user.sub; - Logger.log( - `Getting lock information for database: ${dbname} on host: ${hostUid}`, - 'DatabaseManagementController' + this.logger.log( + `Getting lock information for database: ${dbname} on host: ${hostUid}` ); return await this.managementService.lockDatabase(userId, hostUid, dbname, body); } @@ -393,9 +390,8 @@ export class DatabaseManagementController { validateRequiredFields(body, ['dbuser', 'dbpasswd'], 'database/transaction-info', this.logger); - Logger.log( - `Getting transaction information for database: ${dbname} on host: ${hostUid}`, - 'DatabaseManagementController' + this.logger.log( + `Getting transaction information for database: ${dbname} on host: ${hostUid}` ); return await this.managementService.getTransactionInfo(userId, hostUid, dbname, body); } @@ -439,9 +435,8 @@ export class DatabaseManagementController { ); } - Logger.log( - `Killing transaction for database: ${dbname} on host: ${hostUid}`, - 'DatabaseManagementController' + this.logger.log( + `Killing transaction for database: ${dbname} on host: ${hostUid}` ); return await this.managementService.killTransaction(userId, hostUid, dbname, body); } @@ -472,9 +467,8 @@ export class DatabaseManagementController { validateRequiredFields(body, ['delbackup'], 'database/delete', this.logger); - Logger.log( - `Deleting database: ${dbname} on host: ${hostUid}`, - 'DatabaseManagementController' + this.logger.log( + `Deleting database: ${dbname} on host: ${hostUid}` ); return await this.managementService.deleteDatabase(userId, hostUid, dbname, body); } diff --git a/apps/api-server/src/database/management/database-management.service.ts b/apps/api-server/src/database/management/database-management.service.ts index 67375ee..e5aa882 100644 --- a/apps/api-server/src/database/management/database-management.service.ts +++ b/apps/api-server/src/database/management/database-management.service.ts @@ -37,7 +37,9 @@ import { ConfigErrorCode } from '@error/config/config-error-code'; import { CmsError } from '@error/cms/cms-error'; import { DatabaseError } from '@error/database/database-error'; import { HostService } from '@host'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { BaseService } from '@common'; +import { DATABASE_CONSTANTS } from '../database.constants'; import { AddVolDbCmsRequest, CheckDatabaseCmsRequest, @@ -77,14 +79,14 @@ import { * @since 1.0.0 */ @Injectable() -export class DatabaseManagementService { - private readonly logger = new Logger(DatabaseManagementService.name); - +export class DatabaseManagementService extends BaseService { constructor( - private readonly hostService: HostService, - private readonly cmsClient: CmsHttpsClientService, + hostService: HostService, + cmsClient: CmsHttpsClientService, private readonly databaseConfigService: DatabaseConfigService - ) {} + ) { + super(hostService, cmsClient); + } /** * Unload a database. @@ -105,9 +107,6 @@ export class DatabaseManagementService { dbname: string, request: UnloadDatabaseRequest ): Promise<{}> { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - // Determine target based on isSchemaIncluded and isDataIncluded let target: 'schema' | 'object' | 'both'; @@ -130,7 +129,6 @@ export class DatabaseManagementService { // Build CMS request from client request const cmsRequest: UnloadDatabaseCmsRequest = { task: 'unloaddb', - token: host.token || '', dbname: dbname, targetdir: request.targetdir, target: target, @@ -151,13 +149,10 @@ export class DatabaseManagementService { lofile: request.lofile, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< UnloadDatabaseCmsRequest, UnloadDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); return response.result; } @@ -176,23 +171,16 @@ export class DatabaseManagementService { userId: string, hostUid: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: UnloadInfoCmsRequest = { task: 'unloadinfo', - token: host.token || '', }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< UnloadInfoCmsRequest, UnloadInfoCmsResponse - >(url, request); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, request); - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; + const dataOnly = this.extractDomainData(response); return { database: dataOnly.database || [], @@ -218,13 +206,9 @@ export class DatabaseManagementService { dbname: string, request: LoadDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - // Build CMS request from client request const cmsRequest: LoadDatabaseCmsRequest = { task: 'loaddb', - token: host.token || '', dbname: dbname, checkoption: request.checkoption, period: request.period, @@ -240,10 +224,14 @@ export class DatabaseManagementService { ignoreclassfile: request.ignoreclassfile, }; + const host = await this.hostService.findHostInternal(userId, hostUid); + const url = `https://${host.address}:${host.port}/cm_api`; + const requestWithToken = { ...cmsRequest, token: host.token || '' }; + const response = await this.cmsClient.postAuthenticated< LoadDatabaseCmsRequest, LoadDatabaseCmsResponse - >(url, cmsRequest); + >(url, requestWithToken); checkCmsTokenError(response); @@ -294,24 +282,17 @@ export class DatabaseManagementService { dbname: string, request: OptimizeDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - // Build CMS request from client request const cmsRequest: OptimizeDatabaseCmsRequest = { task: 'optimizedb', - token: host.token || '', dbname: dbname, ...(request.class && { class: request.class }), }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< OptimizeDatabaseCmsRequest, OptimizeDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); // Success: return empty object return {}; @@ -336,23 +317,16 @@ export class DatabaseManagementService { dbname: string, request: CheckDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: CheckDatabaseCmsRequest = { task: 'checkdb', - token: host.token || '', dbname: dbname, repairdb: request.repairdb, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< CheckDatabaseCmsRequest, CheckDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); // Success: return empty object return {}; @@ -377,23 +351,16 @@ export class DatabaseManagementService { dbname: string, request: CompactDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: CompactDatabaseCmsRequest = { task: 'compactdb', - token: host.token || '', dbname: dbname, verbose: request.verbose, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< CompactDatabaseCmsRequest, CompactDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); // Return log if present, otherwise return empty object if (response.log) { @@ -424,13 +391,9 @@ export class DatabaseManagementService { dbname: string, request: RenameDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - // Build CMS request from client request const cmsRequest: RenameDatabaseCmsRequest = { task: 'renamedb', - token: host.token || '', dbname: dbname, rename: request.rename, exvolpath: request.exvolpath, @@ -449,13 +412,10 @@ export class DatabaseManagementService { cmsRequest.volume = [volumeMapping]; } - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< RenameDatabaseCmsRequest, RenameDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); // Success: return empty object return {}; @@ -478,21 +438,15 @@ export class DatabaseManagementService { hostUid: string, dbname: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const request: GetAddVolStatusCmsRequest = { + const cmsRequest: GetAddVolStatusCmsRequest = { task: 'getaddvolstatus', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< GetAddVolStatusCmsRequest, GetAddVolStatusCmsResponse - >(url, request); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); return { freespace: response.freespace, @@ -519,11 +473,8 @@ export class DatabaseManagementService { dbname: string, request: AddVolDbRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; const cmsRequest: AddVolDbCmsRequest = { task: 'addvoldb', - token: host.token || '', dbname: dbname, volname: request.volname, purpose: request.purpose, @@ -532,13 +483,10 @@ export class DatabaseManagementService { size_need_mb: request.size_need_mb, }; - const response = await this.cmsClient.postAuthenticated< + const response = await this.executeCmsRequest< AddVolDbCmsRequest, AddVolDbCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); return { dbname: response.dbname, @@ -565,22 +513,17 @@ export class DatabaseManagementService { dbname: string, request: LockDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: LockDatabaseCmsRequest = { task: 'lockdb', - token: host.token || '', dbname: dbname, }; - const response = await this.cmsClient.postAuthenticated< + this.logger.debug(`Getting lock information for database: ${dbname}`); + + const response = await this.executeCmsRequest< LockDatabaseCmsRequest, LockDatabaseCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); // Return lockinfo only (CMS envelope removed) return { @@ -607,28 +550,23 @@ export class DatabaseManagementService { dbname: string, request: GetTransactionInfoRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: GetTransactionInfoCmsRequest = { task: 'gettransactioninfo', - token: host.token || '', dbname: dbname, dbuser: request.dbuser, dbpasswd: request.dbpasswd, }; - const response = await this.cmsClient.postAuthenticated< + this.logger.debug( + `Getting transaction information for database: ${dbname}, user: ${request.dbuser}` + ); + + const response = await this.executeCmsRequest< GetTransactionInfoCmsRequest, GetTransactionInfoCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - - return dataOnly; + return this.extractDomainData(response); } /** @@ -654,37 +592,42 @@ export class DatabaseManagementService { dbname: string, request: KillTransactionRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - // Validate parameter requirement based on type if (request.type !== 'd' && !request.parameter) { + const typeDescriptions: Record = { + i: 'transaction index', + p: 'process name', + h: 'host name', + }; throw DatabaseError.InvalidParameter( - `Parameter is required for type '${request.type}'`, - { type: request.type, parameter: request.parameter } + `Parameter is required for type '${request.type}' (${typeDescriptions[request.type] || 'unknown type'})`, + { + type: request.type, + parameter: request.parameter, + dbname: dbname, + message: `Missing required parameter for kill transaction type '${request.type}'. Expected: ${typeDescriptions[request.type] || 'parameter'}`, + } ); } // Build CMS request from client request const cmsRequest: KillTransactionCmsRequest = { task: 'killtransaction', - token: host.token || '', dbname: dbname, type: request.type, ...(request.type !== 'd' && request.parameter && { parameter: request.parameter }), }; - const response = await this.cmsClient.postAuthenticated< + this.logger.debug( + `Killing transaction for database: ${dbname} on host: ${hostUid} with type: ${request.type} and parameter: ${request.parameter}` + ); + + const response = await this.executeCmsRequest< KillTransactionCmsRequest, KillTransactionCmsResponse - >(url, cmsRequest); - - checkCmsTokenError(response); - checkCmsStatusError(response); + >(userId, hostUid, cmsRequest); - const { __EXEC_TIME, note, status, task, ...dataOnly } = response; - - return dataOnly; + return this.extractDomainData(response); } /** @@ -707,44 +650,48 @@ export class DatabaseManagementService { dbname: string, request: DeleteDatabaseRequest ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const cmsRequest: DeleteDatabaseCmsRequest = { task: 'deletedb', - token: host.token || '', dbname: dbname, delbackup: request.delbackup, }; - const response = await this.cmsClient.postAuthenticated< - DeleteDatabaseCmsRequest, - DeleteDatabaseCmsResponse - >(url, cmsRequest); + this.logger.debug(`Deleting database: ${dbname} on host: ${hostUid}`); - checkCmsTokenError(response); - checkCmsStatusError(response); + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); // Remove dbname from server parameter in cubridconf if it exists try { await this.databaseConfigService.removeAutoStart(userId, hostUid, { - confname: 'cubridconf', + confname: DATABASE_CONSTANTS.CUBRID_CONF_NAME, dbname: dbname, }); this.logger.debug( - `Removed database name ${dbname} from server parameter in cubridconf` + `Successfully removed database name ${dbname} from server parameter in cubridconf` ); - } catch (error: any) { + } catch (error: unknown) { // Ignore DbnameNotFound error (dbname may not exist in server parameter) // Log other errors but don't fail the delete operation if (error instanceof ConfigError && error.code === ConfigErrorCode.DBNAME_NOT_FOUND) { this.logger.debug( - `Database name ${dbname} not found in server parameter, skipping removal` + `Database name ${dbname} not found in server parameter, skipping removal (this is expected if auto-start was not configured)` ); } else { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = error instanceof ConfigError ? error.code : 'UNKNOWN'; this.logger.warn( - `Failed to remove dbname from server parameter: ${error.message}`, - error.stack + `Failed to remove dbname from server parameter during database deletion: ${errorMessage}`, + { + dbname, + hostUid, + errorCode, + stack: errorStack, + } ); } } diff --git a/apps/api-server/src/database/user/database-user.controller.ts b/apps/api-server/src/database/user/database-user.controller.ts index aa81710..5190ca7 100644 --- a/apps/api-server/src/database/user/database-user.controller.ts +++ b/apps/api-server/src/database/user/database-user.controller.ts @@ -36,7 +36,6 @@ export class DatabaseUserController { @Get() async getDatabaseUsers(@Request() req, @Param('hostUid') hostUid: string) { const userId = req.user.sub; - // TODO: Implement return await this.databaseUserService.getDatabaseUsers(userId); } @@ -65,7 +64,7 @@ export class DatabaseUserController { ): Promise { const userId = req.user.sub; - Logger.log(`Logging in to database: ${dbname} on host: ${hostUid}`, 'DatabaseUserController'); + this.logger.log(`Logging in to database: ${dbname} on host: ${hostUid}`); const result = await this.databaseUserService.loginDatabase( userId, hostUid, @@ -108,9 +107,8 @@ export class DatabaseUserController { this.logger ); - Logger.log( - `Updating user: ${username} in database: ${dbname} on host: ${hostUid}`, - 'DatabaseUserController' + this.logger.log( + `Updating user: ${username} in database: ${dbname} on host: ${hostUid}` ); return await this.databaseUserService.updateUser( userId, diff --git a/apps/api-server/src/database/user/database-user.service.ts b/apps/api-server/src/database/user/database-user.service.ts index f1700e6..a21b2f3 100644 --- a/apps/api-server/src/database/user/database-user.service.ts +++ b/apps/api-server/src/database/user/database-user.service.ts @@ -1,5 +1,5 @@ import { - checkCmsTokenError, + BaseService, HandleCmsHttpsClientErrors, HandleDatabaseErrors, HandleHostErrors, @@ -13,7 +13,6 @@ import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.servic import { BaseCmsResponse } from '@type'; import { LoginDBCmsRequest, UpdateUserCmsRequest } from '@type/cms-request'; import { UpdateUserCmsResponse } from '@type/cms-response'; -import { checkCmsStatusError } from '@common'; /** * Service for managing database users. @@ -22,12 +21,14 @@ import { checkCmsStatusError } from '@common'; * @since 1.0.0 */ @Injectable() -export class DatabaseUserService { +export class DatabaseUserService extends BaseService { constructor( private readonly repository: UserRepositoryService, - private readonly cmsClient: CmsHttpsClientService, - private readonly hostService: HostService - ) {} + protected readonly cmsClient: CmsHttpsClientService, + protected readonly hostService: HostService + ) { + super(hostService, cmsClient); + } /** * Get list of database users for a specific host. @@ -63,23 +64,20 @@ export class DatabaseUserService { const host = await this.hostService.findHostInternal(userId, hostUid); const dbAuth = DBAuthResolver.resolve(host, dbname, clientId, clientPassword); - const url = `https://${host.address}:${host.port}/cm_api`; - const data: LoginDBCmsRequest = { + const cmsRequest: LoginDBCmsRequest = { task: 'dbmtuserlogin', - token: host.token || '', targetid: host.id, dbname: dbAuth.dbname, dbuser: dbAuth.id, dbpasswd: dbAuth.password, }; - const response = await this.cmsClient.postAuthenticated( - url, - data + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest ); - checkCmsTokenError(response); - if (response.status === 'success') { return true; } @@ -113,12 +111,8 @@ export class DatabaseUserService { groups: { group: string[] }, authorization: string[] ): Promise<{}> { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - - const data: UpdateUserCmsRequest = { + const cmsRequest: UpdateUserCmsRequest = { task: 'updateuser', - token: host.token || '', dbname, username, userpass, @@ -126,13 +120,11 @@ export class DatabaseUserService { authorization, }; - const response = await this.cmsClient.postAuthenticated< - UpdateUserCmsRequest, - UpdateUserCmsResponse - >(url, data); - - checkCmsTokenError(response); - checkCmsStatusError(response); + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); return {}; } diff --git a/apps/api-server/src/error/app-error.ts b/apps/api-server/src/error/app-error.ts index 9c1ca1a..0c280ee 100644 --- a/apps/api-server/src/error/app-error.ts +++ b/apps/api-server/src/error/app-error.ts @@ -4,6 +4,9 @@ import { LockErrorCode } from '@error/lock/lock-error-code'; import { HostErrorCode } from '@error/host/host-error-code'; import { UserErrorCode } from '@error/user/user-error-code'; import { DatabaseErrorCode } from '@error/database/database-error-code'; +import { ConfigErrorCode } from '@error/config/config-error-code'; +import { BrokerErrorCode } from '@error/broker/broker-error-code'; +import { CmsErrorCode } from '@error/cms/cms-error-code'; export type ErrorKind = | 'AUTH' @@ -15,7 +18,8 @@ export type ErrorKind = | 'CMS' | 'DATABASE' | 'VALIDATION' - | 'CONFIG'; + | 'CONFIG' + | 'BROKER'; /** * Base error class for all application errors. @@ -70,7 +74,15 @@ export class AppError extends Error { private getSafeFieldsForClient(additionalData: Record): Record { const safeFields: Record = {}; - const allowedFields = ['missingFields', 'dbname', 'bname', 'message']; + const allowedFields = [ + 'missingFields', + 'dbname', + 'bname', + 'message', + 'confname', + 'type', + 'parameter', + ]; const sensitiveFields = [ 'response', @@ -146,6 +158,7 @@ export class AppError extends Error { case HostErrorCode.DUPLICATED_HOST: return 409; // Conflict - resource collision case HostErrorCode.INTERNAL_ERROR: + case HostErrorCode.UNKNOWN: return 500; // Internal Server Error default: return 400; @@ -210,6 +223,8 @@ export class AppError extends Error { return 404; case DatabaseErrorCode.DUPLICATED_DATABASE_PROFILE: return 409; + case DatabaseErrorCode.INVALID_PARAMETER: + return 400; case DatabaseErrorCode.INTERNAL_ERROR: case DatabaseErrorCode.GET_START_INFO_FAILED: case DatabaseErrorCode.START_DATABASE_FAILED: @@ -221,10 +236,47 @@ export class AppError extends Error { default: return 500; } + case 'CONFIG': + switch (this.code) { + case ConfigErrorCode.SERVER_PARAM_NOT_FOUND: + case ConfigErrorCode.NO_CONFLIST_DATA: + case ConfigErrorCode.NO_CONFDATA: + return 404; + case ConfigErrorCode.DBNAME_ALREADY_EXISTS: + return 409; + case ConfigErrorCode.DBNAME_NOT_FOUND: + return 404; + case ConfigErrorCode.GET_ALL_SYS_PARAM_FAILED: + case ConfigErrorCode.SET_SYS_PARAM_FAILED: + case ConfigErrorCode.UNKNOWN: + return 500; + default: + return 500; + } case 'CMS': - return 500; + switch (this.code) { + case CmsErrorCode.INVALID_TOKEN: + return 401; // Unauthorized + case CmsErrorCode.REQUEST_FAILED: + case CmsErrorCode.NO_RESPONSE: + case CmsErrorCode.UNKNOWN: + return 500; // Internal Server Error + default: + return 500; + } case 'VALIDATION': - return 400; + return 400; // All validation errors are bad requests + case 'BROKER': + switch (this.code) { + case BrokerErrorCode.GET_BROKER_FAILED: + case BrokerErrorCode.BROKER_STOP_FAILED: + case BrokerErrorCode.BROKER_START_FAILED: + case BrokerErrorCode.INTERNAL_ERROR: + case BrokerErrorCode.UNKNOWN: + return 500; // Internal Server Error + default: + return 500; + } default: return 500; } diff --git a/apps/api-server/src/error/broker/broker-error-code.ts b/apps/api-server/src/error/broker/broker-error-code.ts index b0cc764..05c8d66 100644 --- a/apps/api-server/src/error/broker/broker-error-code.ts +++ b/apps/api-server/src/error/broker/broker-error-code.ts @@ -1,6 +1,13 @@ +/** + * Enumeration of broker-related error codes. + * + * @category Errors + * @since 1.0.0 + */ export enum BrokerErrorCode { GET_BROKER_FAILED = 'GET_BROKER_FAILED', BROKER_STOP_FAILED = 'BROKER_STOP_FAILED', BROKER_START_FAILED = 'BROKER_START_FAILED', + INTERNAL_ERROR = 'INTERNAL_ERROR', UNKNOWN = 'UNKNOWN', } diff --git a/apps/api-server/src/error/broker/broker-error.ts b/apps/api-server/src/error/broker/broker-error.ts index 5286eb4..aa273e1 100644 --- a/apps/api-server/src/error/broker/broker-error.ts +++ b/apps/api-server/src/error/broker/broker-error.ts @@ -1,9 +1,15 @@ import { AppError } from '@error/app-error'; import { BrokerErrorCode } from './broker-error-code'; +/** + * Error class for broker-related operations. + * + * @category Errors + * @since 1.0.0 + */ export class BrokerError extends AppError { constructor( - kind: 'CMS', + kind: 'BROKER', code: BrokerErrorCode, additionalData?: Record, originalError?: Error @@ -11,29 +17,48 @@ export class BrokerError extends AppError { super(kind, code, additionalData, originalError); } + /** + * Creates an error indicating that getting broker information failed. + */ static GetBrokersFailed(additionalData?: Record, originalError?: Error) { - return new BrokerError('CMS', BrokerErrorCode.GET_BROKER_FAILED, additionalData, originalError); + return new BrokerError('BROKER', BrokerErrorCode.GET_BROKER_FAILED, additionalData, originalError); } + /** + * Creates an error indicating that stopping broker failed. + */ static BrokerStopFailed(additionalData?: Record, originalError?: Error) { return new BrokerError( - 'CMS', + 'BROKER', BrokerErrorCode.BROKER_STOP_FAILED, additionalData, originalError ); } + /** + * Creates an error indicating that starting broker failed. + */ static BrokerStartFailed(additionalData?: Record, originalError?: Error) { return new BrokerError( - 'CMS', + 'BROKER', BrokerErrorCode.BROKER_START_FAILED, additionalData, originalError ); } + /** + * Creates an error for an unknown broker-related issue. + */ static Unknown(additionalData?: Record, originalError?: Error) { - return new BrokerError('CMS', BrokerErrorCode.UNKNOWN, additionalData, originalError); + return new BrokerError('BROKER', BrokerErrorCode.UNKNOWN, additionalData, originalError); + } + + /** + * Creates an error indicating an internal broker-related server error. + */ + static InternalError(additionalData?: Record, originalError?: Error) { + return new BrokerError('BROKER', BrokerErrorCode.INTERNAL_ERROR, additionalData, originalError); } } diff --git a/apps/api-server/src/error/cms/cms-error-code.ts b/apps/api-server/src/error/cms/cms-error-code.ts new file mode 100644 index 0000000..e06f952 --- /dev/null +++ b/apps/api-server/src/error/cms/cms-error-code.ts @@ -0,0 +1,12 @@ +/** + * Enumeration of CMS error codes. + * + * @category Errors + * @since 1.0.0 + */ +export enum CmsErrorCode { + REQUEST_FAILED = 'REQUEST_FAILED', + NO_RESPONSE = 'NO_RESPONSE', + INVALID_TOKEN = 'INVALID_TOKEN', + UNKNOWN = 'UNKNOWN', +} diff --git a/apps/api-server/src/error/cms/cms-error.ts b/apps/api-server/src/error/cms/cms-error.ts index 4570f10..be1e083 100644 --- a/apps/api-server/src/error/cms/cms-error.ts +++ b/apps/api-server/src/error/cms/cms-error.ts @@ -1,17 +1,5 @@ import { AppError } from '@error/app-error'; - -/** - * Enumeration of CMS error codes. - * - * @category Errors - * @since 1.0.0 - */ -export enum CmsErrorCode { - REQUEST_FAILED = 'REQUEST_FAILED', - NO_RESPONSE = 'NO_RESPONSE', - INVALID_TOKEN = 'INVALID_TOKEN', - UNKNOWN = 'UNKNOWN', -} +import { CmsErrorCode } from './cms-error-code'; /** * Represents a CMS-specific error. diff --git a/apps/api-server/src/error/config/config-error.ts b/apps/api-server/src/error/config/config-error.ts index 95e4276..7a1bbb3 100644 --- a/apps/api-server/src/error/config/config-error.ts +++ b/apps/api-server/src/error/config/config-error.ts @@ -47,16 +47,22 @@ export class ConfigError extends AppError { /** * Creates an error indicating that server parameter was not found in configuration file. + * If additionalData.message is provided, it will be used as the error message. */ static ServerParamNotFound( confname: string, additionalData?: Record, originalError?: Error ) { + const defaultMessage = `server parameter not found in configuration file: ${confname}`; return new ConfigError( 'CONFIG', ConfigErrorCode.SERVER_PARAM_NOT_FOUND, - { confname, ...additionalData }, + { + confname, + message: additionalData?.message || defaultMessage, + ...additionalData, + }, originalError ); } diff --git a/apps/api-server/src/error/host/host-error-code.ts b/apps/api-server/src/error/host/host-error-code.ts index 014032e..538d347 100644 --- a/apps/api-server/src/error/host/host-error-code.ts +++ b/apps/api-server/src/error/host/host-error-code.ts @@ -10,4 +10,5 @@ export enum HostErrorCode { DUPLICATED_HOST = 'DUPLICATED_HOST', NO_SUCH_HOST = 'NO_SUCH_HOST', INTERNAL_ERROR = 'INTERNAL_ERROR', + UNKNOWN = 'UNKNOWN', } diff --git a/apps/api-server/src/error/index.ts b/apps/api-server/src/error/index.ts index 75d0c23..2719011 100644 --- a/apps/api-server/src/error/index.ts +++ b/apps/api-server/src/error/index.ts @@ -11,6 +11,7 @@ export * from './database/database-error'; export * from './broker/broker-error'; export * from './auth/auth-error'; export * from './cms/cms-error'; +export * from './cms/cms-error-code'; export * from './validation/validation-error'; export * from './monitoring/resource-monitoring-error'; export * from './monitoring/resource-monitoring-error-code'; diff --git a/apps/api-server/src/error/validation/validation-error-code.ts b/apps/api-server/src/error/validation/validation-error-code.ts index 8c7b219..14570d6 100644 --- a/apps/api-server/src/error/validation/validation-error-code.ts +++ b/apps/api-server/src/error/validation/validation-error-code.ts @@ -1,9 +1,13 @@ /** - * Error codes for validation errors. + * Enumeration of validation-related error codes. + * + * @category Errors + * @since 1.0.0 */ export enum ValidationErrorCode { INVALID_REQUEST_BODY = 'INVALID_REQUEST_BODY', MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD', MISSING_DB_CREDENTIALS = 'MISSING_DB_CREDENTIALS', INVALID_FIELD_FORMAT = 'INVALID_FIELD_FORMAT', + UNKNOWN = 'UNKNOWN', } diff --git a/apps/api-server/src/log/log.service.ts b/apps/api-server/src/log/log.service.ts index 934f875..ecf86a7 100644 --- a/apps/api-server/src/log/log.service.ts +++ b/apps/api-server/src/log/log.service.ts @@ -1,6 +1,6 @@ import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; import { HostService } from '@host'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { GetBrokerLogListClientResponse, ViewLogClientResponse, @@ -16,33 +16,29 @@ import { GetAdminLogInfoCmsResponse, BaseCmsRequest, } from '@type'; -import { checkCmsTokenError, checkCmsStatusError } from '@common'; +import { BaseService } from '@common'; @Injectable() -export class LogService { +export class LogService extends BaseService { constructor( - private readonly client: CmsHttpsClientService, - private readonly hostService: HostService - ) {} + protected readonly client: CmsHttpsClientService, + protected readonly hostService: HostService + ) { + super(hostService, client); + } async getBrokerLogList(userId: string, hostUid: string, bname: string) { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest & { broker: string } = { + const cmsRequest: BaseCmsRequest & { broker: string } = { task: 'getlogfileinfo', - token: host.token || '', broker: bname, }; - const cmsResponse = await this.client.postAuthenticated< + const cmsResponse = await this.executeCmsRequest< BaseCmsRequest & { broker: string }, LogFileInfoCmsResponse - >(url, body); - - checkCmsTokenError(cmsResponse); - checkCmsStatusError(cmsResponse); + >(userId, hostUid, cmsRequest); - Logger.debug(cmsResponse); + this.logger.debug(JSON.stringify(cmsResponse)); const response: GetBrokerLogListClientResponse = { broker: cmsResponse.broker, logfileinfo: cmsResponse.logfileinfo, @@ -51,21 +47,15 @@ export class LogService { } async getDatabaseLogList(userId: string, hostUid: string, dbname: string) { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest & { dbname: string } = { + const cmsRequest: BaseCmsRequest & { dbname: string } = { task: 'getloginfo', - token: host.token || '', dbname: dbname, }; - const cmsResponse = await this.client.postAuthenticated< + const cmsResponse = await this.executeCmsRequest< BaseCmsRequest & { dbname: string }, LogInfoCmsResponse - >(url, body); - - checkCmsTokenError(cmsResponse); - checkCmsStatusError(cmsResponse); + >(userId, hostUid, cmsRequest); const response: GetDatabaseLogListClientResponse = { dbname: cmsResponse.dbname, @@ -75,20 +65,15 @@ export class LogService { } async getCMSLogList(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest = { + const cmsRequest: BaseCmsRequest = { task: 'loadaccesslog', - token: host.token || '', }; - const cmsResponse = await this.client.postAuthenticated< - BaseCmsRequest, - LoadAccessLogCmsResponse - >(url, body); - - checkCmsTokenError(cmsResponse); - checkCmsStatusError(cmsResponse); + const cmsResponse = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); const response: LoadAccessLogClientResponse = { accesslog: cmsResponse.accesslog, @@ -116,26 +101,19 @@ export class LogService { start: string, end: string ): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest & { path: string; start: string; end: string } = { + const cmsRequest: BaseCmsRequest & { path: string; start: string; end: string } = { task: 'viewlog', - token: host.token || '', path: path, start: start, end: end, }; - const cmsResponse = await this.client.postAuthenticated< + const cmsResponse = await this.executeCmsRequest< BaseCmsRequest & { path: string; start: string; end: string }, ViewLogCmsResponse - >(url, body); - - checkCmsTokenError(cmsResponse); - checkCmsStatusError(cmsResponse); + >(userId, hostUid, cmsRequest); - const { __EXEC_TIME, note, status, task, ...dataOnly } = cmsResponse; - return dataOnly; + return this.extractDomainData(cmsResponse); } /** @@ -148,22 +126,16 @@ export class LogService { * @returns GetAdminLogInfoClientResponse Admin log information without CMS envelope fields */ async getAdminLogInfo(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const url = `https://${host.address}:${host.port}/cm_api`; - const body: BaseCmsRequest = { + const cmsRequest: BaseCmsRequest = { task: 'getadminloginfo', - token: host.token || '', }; - const cmsResponse = await this.client.postAuthenticated< - BaseCmsRequest, - GetAdminLogInfoCmsResponse - >(url, body); - - checkCmsTokenError(cmsResponse); - checkCmsStatusError(cmsResponse); + const cmsResponse = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); - const { __EXEC_TIME, note, status, task, ...dataOnly } = cmsResponse; - return dataOnly; + return this.extractDomainData(cmsResponse); } } diff --git a/apps/api-server/src/monitoring/resource-monitoring/resource-monitoring.service.ts b/apps/api-server/src/monitoring/resource-monitoring/resource-monitoring.service.ts index 249ec61..1145b00 100644 --- a/apps/api-server/src/monitoring/resource-monitoring/resource-monitoring.service.ts +++ b/apps/api-server/src/monitoring/resource-monitoring/resource-monitoring.service.ts @@ -1,5 +1,5 @@ import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; -import { HandleResourceMonitoringErrors } from '@common'; +import { BaseService, HandleResourceMonitoringErrors } from '@common'; import { HostService } from '@host'; import { Injectable } from '@nestjs/common'; import { BaseCmsRequest, BaseCmsResponse, CmsGetHostStatResponse } from '@root/src/type'; @@ -12,29 +12,28 @@ import { BaseCmsRequest, BaseCmsResponse, CmsGetHostStatResponse } from '@root/s * @since 1.0.0 */ @Injectable() -export class ResourceMonitoringService { +export class ResourceMonitoringService extends BaseService { constructor( - private readonly client: CmsHttpsClientService, - private readonly hostService: HostService - ) {} + protected readonly client: CmsHttpsClientService, + protected readonly hostService: HostService + ) { + super(hostService, client); + } + @HandleResourceMonitoringErrors() async getHostStat( userId: string, hostUid: string ): Promise> { - const host = await this.hostService.findHostInternal(userId, hostUid); - - const url = `https://${host.address}:${host.port}/cm_api`; - const body = { - token: host.token || '', + const cmsRequest: BaseCmsRequest = { task: 'gethoststat', }; - const response = await this.client.postAuthenticated( - url, - body + const response = await this.executeCmsRequest( + userId, + hostUid, + cmsRequest ); - const { __EXEC_TIME, task, status, note, ...dataOnly } = response; - return dataOnly; + return this.extractDomainData(response); } } diff --git a/apps/api-server/src/type/cms-request/base-cms-request.ts b/apps/api-server/src/type/cms-request/base-cms-request.ts index b2190bd..e77473b 100644 --- a/apps/api-server/src/type/cms-request/base-cms-request.ts +++ b/apps/api-server/src/type/cms-request/base-cms-request.ts @@ -1,10 +1,11 @@ /** - * Represents the base structure for a CMS request, including a task and an authentication token. + * Represents the base structure for a CMS request, including a task and an optional authentication token. + * The token is optional because it can be automatically added by BaseService.executeCmsRequest. * * @category Requests * @since 1.0.0 */ export type BaseCmsRequest = { task: string; - token: string; + token?: string; }; diff --git a/package.json b/package.json index 22df8ee..447f99a 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,11 @@ "@user": "apps/api-server/src/user/index", "@util": "apps/api-server/src/util/index" }, + "scripts": { + "serve:web-manager": "node serve-web-manager.js", + "serve:web-manager:4200": "node serve-web-manager.js 4200", + "serve:web-manager:8080": "node serve-web-manager.js 8080" + }, "pkg": { "scripts": [ "main.js" diff --git a/serve-web-manager.js b/serve-web-manager.js new file mode 100644 index 0000000..4a2d193 --- /dev/null +++ b/serve-web-manager.js @@ -0,0 +1,118 @@ +#!/usr/bin/env node + +/** + * 간단한 정적 파일 서버 + * web-manager 빌드 결과물을 서빙합니다. + * + * 사용법: + * node serve-web-manager.js [포트번호] + * 예: node serve-web-manager.js 4200 + */ + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = process.argv[2] ? parseInt(process.argv[2], 10) : 4200; +const BUILD_DIR = path.join(__dirname, 'dist', 'apps', 'web-manager'); + +// MIME 타입 매핑 +const mimeTypes = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.eot': 'application/vnd.ms-fontobject', +}; + +function getMimeType(filePath) { + const ext = path.extname(filePath).toLowerCase(); + return mimeTypes[ext] || 'application/octet-stream'; +} + +function serveFile(filePath, res) { + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('404 Not Found'); + return; + } + + const mimeType = getMimeType(filePath); + res.writeHead(200, { 'Content-Type': mimeType }); + res.end(data); + }); +} + +const server = http.createServer((req, res) => { + // CORS 헤더 추가 (필요한 경우) + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // URL 정규화 + let filePath = req.url === '/' ? '/index.html' : req.url; + filePath = filePath.split('?')[0]; // 쿼리 문자열 제거 + + // 실제 파일 경로 + const fullPath = path.join(BUILD_DIR, filePath); + + // 보안: BUILD_DIR 밖으로 나가는 경로 차단 + if (!fullPath.startsWith(BUILD_DIR)) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('403 Forbidden'); + return; + } + + fs.stat(fullPath, (err, stats) => { + if (err || !stats.isFile()) { + // 파일이 없으면 index.html로 fallback (SPA 라우팅 지원) + const indexPath = path.join(BUILD_DIR, 'index.html'); + serveFile(indexPath, res); + return; + } + + serveFile(fullPath, res); + }); +}); + +// 빌드 디렉토리 확인 +if (!fs.existsSync(BUILD_DIR)) { + console.error(`❌ 빌드 디렉토리를 찾을 수 없습니다: ${BUILD_DIR}`); + console.error('먼저 다음 명령어로 빌드하세요:'); + console.error(' nx build web-manager'); + process.exit(1); +} + +server.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Web Manager 서버가 시작되었습니다!`); + console.log(`📁 서빙 디렉토리: ${BUILD_DIR}`); + console.log(`🌐 접속 주소: http://localhost:${PORT}`); + console.log(` 또는: http://0.0.0.0:${PORT}`); + console.log(`\n종료하려면 Ctrl+C를 누르세요.`); +}); + +// 에러 처리 +server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`❌ 포트 ${PORT}가 이미 사용 중입니다.`); + console.error(`다른 포트를 사용하세요: node serve-web-manager.js [포트번호]`); + } else { + console.error('❌ 서버 오류:', err); + } + process.exit(1); +}); From 908b727fa5174aad6b2433f1f18436deaece2bba Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Thu, 5 Mar 2026 11:11:32 +0900 Subject: [PATCH 02/11] misc : bugs fixed --- .../database/user/database-user.controller.ts | 32 ++++++--- apps/api-server/src/user/user.service.ts | 66 ++++++++++++++----- .../src/response/user-response.ts | 2 +- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/apps/api-server/src/database/user/database-user.controller.ts b/apps/api-server/src/database/user/database-user.controller.ts index 5190ca7..d7d7138 100644 --- a/apps/api-server/src/database/user/database-user.controller.ts +++ b/apps/api-server/src/database/user/database-user.controller.ts @@ -64,15 +64,29 @@ export class DatabaseUserController { ): Promise { const userId = req.user.sub; - this.logger.log(`Logging in to database: ${dbname} on host: ${hostUid}`); - const result = await this.databaseUserService.loginDatabase( - userId, - hostUid, - dbname, - body.id, - body.password - ); - return result; + // Try to validate id/password - if missing, will use profile instead + try { + validateRequiredFields(body, ['id', 'password'], `database/users/login/${dbname}`, this.logger); + // If validation passes, use provided credentials + this.logger.log(`Logging in to database: ${dbname} on host: ${hostUid}`); + return await this.databaseUserService.loginDatabase( + userId, + hostUid, + dbname, + body.id, + body.password + ); + } catch (error) { + // If ValidationError (id/password are missing), don't pass parameters to use profile + // DBAuthResolver.resolve uses == null check, so undefined is handled correctly + if (error instanceof ValidationError) { + this.logger.log(`Logging in to database: ${dbname} on host: ${hostUid} (using profile)`); + return await this.databaseUserService.loginDatabase(userId, hostUid, dbname); + } else { + // Re-throw non-validation errors + throw error; + } + } } /** diff --git a/apps/api-server/src/user/user.service.ts b/apps/api-server/src/user/user.service.ts index 953e44e..9e02e24 100644 --- a/apps/api-server/src/user/user.service.ts +++ b/apps/api-server/src/user/user.service.ts @@ -1,21 +1,21 @@ +import { + ChangePasswordRequest, + DeleteUserRequest, + UpdateUserInfoRequest, + UserResponse, +} from '@api-interfaces'; +import { HandleUserErrors } from '@common'; +import { UserError } from '@error/user/user-error'; import { Injectable } from '@nestjs/common'; import { UserRepositoryService } from '@repository'; import { PasswordService } from '@security'; -import { UserError } from '@error/user/user-error'; -import { passwordValidityChecker, omitPassword } from '@util'; -import { HandleUserErrors } from '@common'; import { - User, UpdateUserDto, + User, UserPreference, UserPreferenceDto, } from '@type/index'; -import { - ChangePasswordRequest, - DeleteUserRequest, - UpdateUserInfoRequest, - UserResponse, -} from '@api-interfaces'; +import { passwordValidityChecker } from '@util'; /** * Service for managing user-related operations. @@ -71,24 +71,60 @@ export class UserService { } /** - * Retrieves user data excluding the password field. + * Retrieves user data excluding sensitive information. * - * Loads user information from the repository and removes the password field - * for security purposes before returning the data. + * Loads user information from the repository and removes: + * - User password + * - Host passwords and tokens in host_list + * - Database passwords in dbProfiles within each host * * @param {string} userId - The unique identifier of the user - * @returns {Promise} User data without password + * @returns {Promise} User data without sensitive information * @throws {UserError} When user is not found * @example * ```typescript * const userData = await userService.getUserData("user123"); * console.log(userData.department); // "IT" * // userData.password is undefined + * // userData.host_list[uid].password is undefined + * // userData.host_list[uid].token is undefined + * // userData.host_list[uid].dbProfiles[dbname].password is undefined * ``` */ @HandleUserErrors() async getUserData(userId: string): Promise { - return omitPassword(await this.repository.loadUserById(userId)); + const user = await this.repository.loadUserById(userId); + + // Remove user password and uuid to match UserResponse type + const { password, uuid, ...userResponse } = user; + + // Process host_list: remove password, token, and dbProfiles passwords + const sanitizedHostList: Record = {}; + for (const [hostUid, hostInfo] of Object.entries(userResponse.host_list)) { + const { password, token, dbProfiles, ...hostWithoutSensitive } = hostInfo; + + // Process dbProfiles: remove passwords from each DB profile + const sanitizedDbProfiles: Record = {}; + if (dbProfiles) { + for (const [dbname, dbInfo] of Object.entries(dbProfiles)) { + const { password: dbPassword, ...dbWithoutPassword } = dbInfo; + sanitizedDbProfiles[dbname] = dbWithoutPassword; + } + } + + sanitizedHostList[hostUid] = { + ...hostWithoutSensitive, + dbProfiles: sanitizedDbProfiles, + }; + } + + // Return with sanitized host_list, ensuring uuid is not included + const result: UserResponse = { + ...userResponse, + host_list: sanitizedHostList, + }; + + return result; } /** diff --git a/libs/api-interfaces/src/response/user-response.ts b/libs/api-interfaces/src/response/user-response.ts index cff46d0..17548ac 100644 --- a/libs/api-interfaces/src/response/user-response.ts +++ b/libs/api-interfaces/src/response/user-response.ts @@ -9,4 +9,4 @@ import { User } from '@type/user'; * @category Responses * @since 1.0.0 */ -export type UserResponse = Omit; +export type UserResponse = Omit; From 57286c3a61a7b9e873756f287d4d7efb471544cc Mon Sep 17 00:00:00 2001 From: JohnDohnut <51821505+JohnDohnut@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:37:35 +0900 Subject: [PATCH 03/11] Delete INDEX_SIGNATURE_EXPLANATION.md --- INDEX_SIGNATURE_EXPLANATION.md | 301 --------------------------------- 1 file changed, 301 deletions(-) delete mode 100644 INDEX_SIGNATURE_EXPLANATION.md diff --git a/INDEX_SIGNATURE_EXPLANATION.md b/INDEX_SIGNATURE_EXPLANATION.md deleted file mode 100644 index 09fdd67..0000000 --- a/INDEX_SIGNATURE_EXPLANATION.md +++ /dev/null @@ -1,301 +0,0 @@ -# TypeScript 인덱스 시그니처(Index Signature) 설명 - -## 인덱스 시그니처란? - -인덱스 시그니처는 **객체의 동적 키(dynamic key)를 타입으로 정의**하는 TypeScript의 기능입니다. - -## 기본 문법 - -```typescript -{ - [key: string]: 타입; -} -// 또는 -{ - [key: number]: 타입; -} -``` - -## 간단한 예제 - -### 예제 1: 기본 사용법 - -```typescript -// 인덱스 시그니처 사용 -type MyObject = { - [key: string]: number; -}; - -const obj: MyObject = { - "name": 1, // ✅ OK - "age": 2, // ✅ OK - "city": 3, // ✅ OK - "anyKey": 100 // ✅ OK - 어떤 문자열 키든 가능 -}; - -// obj["anything"] = 200; // ✅ OK -``` - -### 예제 2: 실제 사용 사례 - -```typescript -// 사용자 정의 속성 객체 -type UserPreferences = { - theme: string; // 명시적 속성 - language: string; // 명시적 속성 - - [key: string]: string; // 인덱스 시그니처: 나머지 모든 키는 string -}; - -const prefs: UserPreferences = { - theme: "dark", - language: "ko", - fontSize: "large", // ✅ OK - 인덱스 시그니처로 허용됨 - colorScheme: "blue" // ✅ OK -}; -``` - -## 우리 프로젝트의 경우 - -### GetBackupInfoCmsResponse 타입 - -```typescript -export type GetBackupInfoCmsResponse = BaseCmsResponse & { - dbname: string; // 명시적 속성: 데이터베이스 이름 - - // 인덱스 시그니처: 동적 키로 백업 정보 배열을 저장 - [key: string]: string | BackupInfo[]; -}; -``` - -### 실제 응답 구조 - -```json -{ - "__EXEC_TIME": "100ms", - "status": "success", - "task": "getbackupinfo", - "dbname": "demodb", // ← 명시적 속성 (string) - "demodb": [ // ← 동적 키 (BackupInfo[]) - { "backupid": "1", ... }, - { "backupid": "2", ... } - ], - "testdb": [ // ← 다른 DB도 동적 키로 가능 - { "backupid": "3", ... } - ] -} -``` - -## 문제 상황 - -### 타입 충돌 - -```typescript -type GetBackupInfoCmsResponse = { - dbname: string; // 명시적: string - - [key: string]: string | BackupInfo[]; // 인덱스 시그니처: 모든 키는 string | BackupInfo[] -}; -``` - -**문제점:** -- `dbname`은 명시적으로 `string`으로 정의했지만 -- 인덱스 시그니처 `[key: string]: string | BackupInfo[]`가 **모든 문자열 키**를 포함 -- TypeScript는 `dbname`도 인덱스 시그니처의 범위에 포함시킴 -- 결과: `dbname`의 타입이 `string | BackupInfo[]`로 추론됨 - -### 시각적 설명 - -``` -타입 정의: -┌─────────────────────────────────────────┐ -│ dbname: string │ ← 명시적 정의 -│ [key: string]: string | BackupInfo[] │ ← 인덱스 시그니처 -└─────────────────────────────────────────┘ - ↓ -TypeScript의 타입 추론: -┌─────────────────────────────────────────┐ -│ 모든 문자열 키 → string | BackupInfo[] │ -│ dbname도 문자열 키이므로 포함됨! │ -└─────────────────────────────────────────┘ - ↓ -최종 타입: -┌─────────────────────────────────────────┐ -│ dbname: string | BackupInfo[] ❌ │ ← 문제! -└─────────────────────────────────────────┘ -``` - -## 해결 방법 - -### 방법 1: 타입 단언 (현재 사용 중) - -```typescript -// extractDomainData 전에 dbname을 먼저 추출 -const responseDbname = response.dbname as string; // 명시적 타입 단언 -``` - -**장점:** -- 간단하고 명확 -- 실제로 `dbname`은 항상 `string`이므로 안전 - -**단점:** -- 타입 단언은 런타임 검증을 하지 않음 - -### 방법 2: 타입 정의 개선 (권장) - -```typescript -export type GetBackupInfoCmsResponse = BaseCmsResponse & { - dbname: string; // 명시적 속성 - - // 인덱스 시그니처를 선택적(optional)으로 만들고 dbname 제외 - [key: string]: string | BackupInfo[] | undefined; -} & { - // dbname을 명시적으로 string으로 보장 - dbname: string; -}; -``` - -또는 더 나은 방법: - -```typescript -export type GetBackupInfoCmsResponse = BaseCmsResponse & { - dbname: string; // 명시적 속성 -} & { - // dbname을 제외한 나머지 동적 키들 - [K in Exclude]?: BackupInfo[]; -}; -``` - -## 인덱스 시그니처의 특징 - -### 1. 모든 키를 포함 - -```typescript -type Example = { - name: string; - [key: string]: string | number; -}; - -// name도 인덱스 시그니처의 범위에 포함됨 -// name: string | number (string이 포함되므로 OK) -``` - -### 2. 타입 호환성 - -```typescript -type A = { - [key: string]: string | number; -}; - -type B = { - name: string; // string은 string | number에 호환됨 ✅ - age: number; // number는 string | number에 호환됨 ✅ - city: boolean; // ❌ 오류! boolean은 string | number에 포함되지 않음 -}; -``` - -### 3. 읽기 전용 인덱스 시그니처 - -```typescript -type ReadOnly = { - readonly [key: string]: string; -}; - -const obj: ReadOnly = { name: "test" }; -// obj.name = "new"; // ❌ 오류! 읽기 전용 -``` - -## 실제 사용 예제 - -### 예제 1: 설정 객체 - -```typescript -type Config = { - apiUrl: string; - timeout: number; - [key: string]: string | number; // 추가 설정 허용 -}; - -const config: Config = { - apiUrl: "https://api.example.com", - timeout: 5000, - retryCount: 3, // ✅ 동적 추가 가능 - maxConnections: 10 // ✅ 동적 추가 가능 -}; -``` - -### 예제 2: 번역 객체 - -```typescript -type Translations = { - [key: string]: string; // 모든 키가 문자열 값 -}; - -const translations: Translations = { - "hello": "안녕하세요", - "goodbye": "안녕히 가세요", - "welcome": "환영합니다" - // 어떤 키든 추가 가능 -}; -``` - -### 예제 3: 우리 프로젝트처럼 동적 키 - -```typescript -type DatabaseBackups = { - dbname: string; // 명시적 속성 - [dbName: string]: BackupInfo[]; // 동적 키: DB 이름별 백업 정보 -}; - -// 실제 데이터: -{ - dbname: "demodb", - "demodb": [backup1, backup2], - "testdb": [backup3, backup4] -} -``` - -## 주의사항 - -### 1. 타입 안전성 감소 - -인덱스 시그니처를 사용하면 타입 체크가 느슨해집니다: - -```typescript -type Loose = { - [key: string]: any; // 모든 타입 허용 -}; - -const obj: Loose = { - name: "test", - age: 30, - isValid: true -}; - -// obj.anything = "anything"; // ✅ 컴파일은 통과하지만 위험할 수 있음 -``` - -### 2. 명시적 속성과의 충돌 - -```typescript -type Conflicting = { - name: string; - [key: string]: number; // ❌ 오류! name은 string인데 인덱스 시그니처는 number -}; -``` - -**해결:** -```typescript -type Fixed = { - name: string; - [key: string]: string | number; // ✅ string과 number 모두 허용 -}; -``` - -## 요약 - -1. **인덱스 시그니처**는 동적 키를 타입으로 정의하는 기능 -2. `[key: string]: 타입` 형태로 사용 -3. **모든 문자열 키**에 적용되므로 명시적 속성과 충돌할 수 있음 -4. 우리 프로젝트에서는 `dbname`이 인덱스 시그니처에 포함되어 타입 오류 발생 -5. 해결: 타입 단언 또는 타입 정의 개선 From 9c869a61c8b2c34b4c6626cf40516c079b2a8b6c Mon Sep 17 00:00:00 2001 From: JohnDohnut <51821505+JohnDohnut@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:37:46 +0900 Subject: [PATCH 04/11] Delete SERVE_WEB_MANAGER.md --- SERVE_WEB_MANAGER.md | 236 ------------------------------------------- 1 file changed, 236 deletions(-) delete mode 100644 SERVE_WEB_MANAGER.md diff --git a/SERVE_WEB_MANAGER.md b/SERVE_WEB_MANAGER.md deleted file mode 100644 index 0a51594..0000000 --- a/SERVE_WEB_MANAGER.md +++ /dev/null @@ -1,236 +0,0 @@ -# Web Manager 서빙 가이드 - -nginx 없이 Node.js만 사용해서 web-manager를 서빙하는 방법입니다. - -## 방법 1: 제공된 스크립트 사용 (추천) - -### 1단계: 프론트엔드 빌드 - -```bash -nx build web-manager -``` - -### 2단계: 서버 실행 - -```bash -# 기본 포트(4200)로 실행 -npm run serve:web-manager - -# 또는 직접 실행 -node serve-web-manager.js - -# 다른 포트로 실행 -node serve-web-manager.js 8080 -``` - -### 3단계: 접속 - -브라우저에서 `http://서버IP:4200` (또는 지정한 포트)로 접속하세요. - ---- - -## 방법 2: npx serve 사용 (가장 간단) - -### 1단계: 프론트엔드 빌드 - -```bash -nx build web-manager -``` - -### 2단계: 서버 실행 - -```bash -# 기본 포트(3000)로 실행 -npx serve dist/apps/web-manager - -# 특정 포트로 실행 -npx serve dist/apps/web-manager -l 4200 - -# 모든 인터페이스에서 접속 가능하게 (0.0.0.0) -npx serve dist/apps/web-manager -l tcp://0.0.0.0:4200 -``` - -### 3단계: 접속 - -브라우저에서 `http://서버IP:4200`로 접속하세요. - ---- - -## 방법 3: npx http-server 사용 - -### 1단계: 프론트엔드 빌드 - -```bash -nx build web-manager -``` - -### 2단계: 서버 실행 - -```bash -# 기본 포트(8080)로 실행 -npx http-server dist/apps/web-manager - -# 특정 포트로 실행 -npx http-server dist/apps/web-manager -p 4200 - -# 모든 인터페이스에서 접속 가능하게 -npx http-server dist/apps/web-manager -p 4200 -a 0.0.0.0 -``` - -### 3단계: 접속 - -브라우저에서 `http://서버IP:4200`로 접속하세요. - ---- - -## 방법 4: Vite Preview 사용 - -### 1단계: 프론트엔드 빌드 - -```bash -nx build web-manager -``` - -### 2단계: Preview 서버 실행 - -```bash -# vite.config.mts의 preview 설정 사용 (기본 포트 4200) -npx vite preview --outDir dist/apps/web-manager - -# 특정 포트로 실행 -npx vite preview --outDir dist/apps/web-manager --port 4200 - -# 모든 인터페이스에서 접속 가능하게 -npx vite preview --outDir dist/apps/web-manager --port 4200 --host 0.0.0.0 -``` - -### 3단계: 접속 - -브라우저에서 `http://서버IP:4200`로 접속하세요. - ---- - -## 리눅스 서버에서 백그라운드 실행 - -### 방법 1: nohup 사용 - -```bash -# 방법 1 (제공된 스크립트) -nohup node serve-web-manager.js 4200 > web-manager.log 2>&1 & - -# 방법 2 (npx serve) -nohup npx serve dist/apps/web-manager -l tcp://0.0.0.0:4200 > web-manager.log 2>&1 & - -# 방법 3 (npx http-server) -nohup npx http-server dist/apps/web-manager -p 4200 -a 0.0.0.0 > web-manager.log 2>&1 & -``` - -### 방법 2: screen 사용 - -```bash -# screen 세션 시작 -screen -S web-manager - -# 서버 실행 -node serve-web-manager.js 4200 - -# Ctrl+A, D로 detach (백그라운드로 전환) -# 다시 접속하려면: screen -r web-manager -``` - -### 방법 3: tmux 사용 - -```bash -# tmux 세션 시작 -tmux new -s web-manager - -# 서버 실행 -node serve-web-manager.js 4200 - -# Ctrl+B, D로 detach -# 다시 접속하려면: tmux attach -t web-manager -``` - ---- - -## systemd 서비스로 등록 (선택사항) - -`/etc/systemd/system/web-manager.service` 파일 생성: - -```ini -[Unit] -Description=Web Manager Frontend Server -After=network.target - -[Service] -Type=simple -User=your-username -WorkingDirectory=/path/to/cubrid-webmanager -ExecStart=/usr/bin/node /path/to/cubrid-webmanager/serve-web-manager.js 4200 -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -``` - -서비스 시작: - -```bash -sudo systemctl daemon-reload -sudo systemctl enable web-manager -sudo systemctl start web-manager -sudo systemctl status web-manager -``` - ---- - -## 방화벽 설정 - -리눅스 서버에서 포트를 열어야 할 수 있습니다: - -```bash -# UFW 사용 시 -sudo ufw allow 4200/tcp - -# firewalld 사용 시 -sudo firewall-cmd --permanent --add-port=4200/tcp -sudo firewall-cmd --reload - -# iptables 사용 시 -sudo iptables -A INPUT -p tcp --dport 4200 -j ACCEPT -``` - ---- - -## 주의사항 - -1. **프로덕션 환경**에서는 nginx나 Apache 같은 웹 서버 사용을 권장합니다. -2. **HTTPS**가 필요한 경우 nginx를 리버스 프록시로 사용하거나, Node.js에서 HTTPS를 직접 설정해야 합니다. -3. **보안**: 개발/테스트 목적으로만 사용하고, 프로덕션에서는 적절한 보안 설정을 추가하세요. - ---- - -## 문제 해결 - -### 포트가 이미 사용 중인 경우 - -```bash -# 포트 사용 중인 프로세스 확인 -sudo lsof -i :4200 -# 또는 -sudo netstat -tulpn | grep 4200 - -# 프로세스 종료 -kill -9 -``` - -### 빌드 디렉토리가 없는 경우 - -```bash -# 빌드 실행 -nx build web-manager - -# 빌드 확인 -ls -la dist/apps/web-manager -``` From f41a9518f1e8cee46b67fb94ed97742c1dcd17f1 Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Thu, 5 Mar 2026 16:44:43 +0900 Subject: [PATCH 05/11] fix : SSL cert logic modified for testing --- apps/api-server/src/util/ssl-util.ts | 41 ++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/api-server/src/util/ssl-util.ts b/apps/api-server/src/util/ssl-util.ts index c3356de..a7f03fc 100644 --- a/apps/api-server/src/util/ssl-util.ts +++ b/apps/api-server/src/util/ssl-util.ts @@ -24,12 +24,45 @@ export function getOrCreateSSLCert() { console.log('\t@ certPath : ', certPath); console.log('\t@ keyPath : ', keyPath); + // Get server IP from environment or network interfaces + const getServerIPs = (): string[] => { + const ips: string[] = ['127.0.0.1']; // Always include localhost + + // Try to get IP from environment variable + const envIP = process.env.SERVER_IP; + if (envIP) { + ips.push(envIP); + } + + // Try to get IPs from network interfaces + try { + const os = require('os'); + const interfaces = os.networkInterfaces(); + for (const name of Object.keys(interfaces)) { + for (const iface of interfaces[name] || []) { + // Skip internal (i.e. 127.0.0.1) and non-IPv4 addresses + if (iface.family === 'IPv4' && !iface.internal) { + ips.push(iface.address); + } + } + } + } catch (error) { + console.warn('Failed to get network interfaces:', error); + } + + // Remove duplicates + return [...new Set(ips)]; + }; + + const serverIPs = getServerIPs(); + console.log('Server IPs for SSL certificate:', serverIPs); + const certExtensions = [ { name: 'subjectAltName', altNames: [ { type: 2, value: 'localhost' }, - { type: 7, ip: '127.0.0.1' }, + ...serverIPs.map(ip => ({ type: 7, ip })), ], }, ]; @@ -44,7 +77,11 @@ export function getOrCreateSSLCert() { }); fs.writeFileSync(certPath, pems.cert); fs.writeFileSync(keyPath, pems.private); - console.log('SSL created'); + console.log('SSL created with IPs:', serverIPs); + } else { + // Check if certificate needs to be regenerated with new IPs + // For now, we'll just log - in production you might want to check cert validity + console.log('Using existing SSL certificate'); } return { From c47eb3097b5a3a9de6079df4a109cbcdd122cb3f Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Mon, 9 Mar 2026 08:38:06 +0900 Subject: [PATCH 06/11] fix : serve-web-manager.js moved into tools directory --- package.json | 6 +++--- serve-web-manager.js => tools/serve-web-manager.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename serve-web-manager.js => tools/serve-web-manager.js (97%) diff --git a/package.json b/package.json index 447f99a..1e298bf 100644 --- a/package.json +++ b/package.json @@ -129,9 +129,9 @@ "@util": "apps/api-server/src/util/index" }, "scripts": { - "serve:web-manager": "node serve-web-manager.js", - "serve:web-manager:4200": "node serve-web-manager.js 4200", - "serve:web-manager:8080": "node serve-web-manager.js 8080" + "serve:web-manager": "node tools/serve-web-manager.js", + "serve:web-manager:4200": "node tools/serve-web-manager.js 4200", + "serve:web-manager:8080": "node tools/serve-web-manager.js 8080" }, "pkg": { "scripts": [ diff --git a/serve-web-manager.js b/tools/serve-web-manager.js similarity index 97% rename from serve-web-manager.js rename to tools/serve-web-manager.js index 4a2d193..23e2348 100644 --- a/serve-web-manager.js +++ b/tools/serve-web-manager.js @@ -14,7 +14,7 @@ const fs = require('fs'); const path = require('path'); const PORT = process.argv[2] ? parseInt(process.argv[2], 10) : 4200; -const BUILD_DIR = path.join(__dirname, 'dist', 'apps', 'web-manager'); +const BUILD_DIR = path.join(__dirname, '..', 'dist', 'apps', 'web-manager'); // MIME 타입 매핑 const mimeTypes = { From bcaad993ac4c988577b65f2e225611c32f63fe7d Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Mon, 9 Mar 2026 08:40:19 +0900 Subject: [PATCH 07/11] fix : script cmd name changed --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1e298bf..f4f3d3b 100644 --- a/package.json +++ b/package.json @@ -129,9 +129,9 @@ "@util": "apps/api-server/src/util/index" }, "scripts": { - "serve:web-manager": "node tools/serve-web-manager.js", - "serve:web-manager:4200": "node tools/serve-web-manager.js 4200", - "serve:web-manager:8080": "node tools/serve-web-manager.js 8080" + "dev": "node tools/serve-web-manager.js", + "dev:4200": "node tools/serve-web-manager.js 4200", + "dev:8080": "node tools/serve-web-manager.js 8080" }, "pkg": { "scripts": [ From 9e6f41655f7c2cd9bed8a6385af2e688bf7b8886 Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Mon, 9 Mar 2026 10:27:30 +0900 Subject: [PATCH 08/11] refactoring : deletedb moved into database-lifecycle module --- .../database-lifecycle.controller.ts | 33 ++++- .../database-lifecycle.service.spec.ts | 117 +++++++++++++++++- .../lifecycle/database-lifecycle.service.ts | 77 ++++++++++++ .../database-management.controller.ts | 35 +----- .../database-management.service.spec.ts | 113 ----------------- .../management/database-management.service.ts | 76 ------------ 6 files changed, 226 insertions(+), 225 deletions(-) diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts index f163da2..d271ad0 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts @@ -1,10 +1,12 @@ -import { Body, Controller, Get, Logger, Param, Post, Request } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Logger, Param, Post, Request } from '@nestjs/common'; import { CreateDatabaseClientRequest, CreateDatabaseClientResponse, CreateDatabaseWithConfigRequest, CreateDatabaseWithConfigResponse, DatabaseVolumeInfoClientResponse, + DeleteDatabaseRequest, + DeleteDatabaseResponse, GetCreatedbInfoClientResponse, StartInfoClientResponse, SaveDatabaseProfileRequest, @@ -256,4 +258,33 @@ export class DatabaseLifecycleController { const response = await this.lifecycleService.getDBSpaceInfo(userId, hostUid, dbname); return response; } + + /** + * Delete a database on a host. + * Also removes the database name from the server parameter in cubridconf if it exists. + * + * @route DELETE /:hostUid/database/:dbname + * @param req Express request (contains authenticated user) + * @param hostUid Host unique identifier from path parameter + * @param dbname Database name from path parameter + * @param body Request body containing delbackup option + * @returns DeleteDatabaseResponse Empty object on success + * @example + * // DELETE /host-uid/database/testdb + * // Body: { "delbackup": "y" } + */ + @Delete(':dbname') + async deleteDatabase( + @Request() req, + @Param('hostUid') hostUid: string, + @Param('dbname') dbname: string, + @Body() body: DeleteDatabaseRequest + ): Promise { + const userId = req.user.sub; + + validateRequiredFields(body, ['delbackup'], 'database/delete', this.logger); + + this.logger.log(`Deleting database: ${dbname} on host: ${hostUid}`); + return await this.lifecycleService.deleteDatabase(userId, hostUid, dbname, body); + } } diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts index 7990a88..ba9cc48 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts @@ -9,7 +9,9 @@ import { DatabaseUserService } from '../user/database-user.service'; import { DatabaseConfigService } from '../config/database-config.service'; import { DatabaseError } from '@error/database/database-error'; import { HostError } from '@error/index'; -import { CreateDatabaseClientResponse } from '@api-interfaces'; +import { CmsError } from '@error/cms/cms-error'; +import { CreateDatabaseClientResponse, DeleteDatabaseRequest } from '@api-interfaces'; +import { DeleteDatabaseCmsResponse } from '@type/cms-response'; import * as common from '@common'; // Mock the checkCmsTokenError and checkCmsStatusError functions @@ -65,6 +67,7 @@ describe('DatabaseLifecycleService', () => { const mockDatabaseConfigService = { setAutoAddVol: jest.fn(), setAutoStart: jest.fn(), + removeAutoStart: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ @@ -775,4 +778,116 @@ describe('DatabaseLifecycleService', () => { expect(result.setAutoStart?.error?.message).toBe('Auto-start failed'); }); }); + + describe('deleteDatabase', () => { + const mockSuccessResponse: DeleteDatabaseCmsResponse = { + __EXEC_TIME: '848 ms', + note: 'none', + status: 'success', + task: 'deletedb', + }; + + it('should successfully delete database with delbackup "y"', async () => { + cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); + const request: DeleteDatabaseRequest = { + delbackup: 'y', + }; + + const result = await service.deleteDatabase( + mockUserId, + mockHostUid, + mockDbname, + request + ); + + expect(hostService.findHostInternal).toHaveBeenCalledWith(mockUserId, mockHostUid); + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + `https://${mockHost.address}:${mockHost.port}/cm_api`, + { + task: 'deletedb', + token: mockHost.token, + dbname: mockDbname, + delbackup: 'y', + } + ); + expect(result).toEqual({}); + }); + + it('should successfully delete database with delbackup "n"', async () => { + cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); + const request: DeleteDatabaseRequest = { + delbackup: 'n', + }; + + const result = await service.deleteDatabase( + mockUserId, + mockHostUid, + mockDbname, + request + ); + + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + task: 'deletedb', + delbackup: 'n', + }) + ); + expect(result).toEqual({}); + }); + + it('should throw HostError if host is not found', async () => { + hostService.findHostInternal.mockRejectedValue( + HostError.NoSuchHost({ hostUid: mockHostUid }) + ); + const request: DeleteDatabaseRequest = { + delbackup: 'y', + }; + + await expect( + service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) + ).rejects.toThrow(HostError); + }); + + it('should throw CmsError if CMS request fails', async () => { + cmsClient.postAuthenticated.mockRejectedValue( + CmsError.RequestFailed({ message: 'CMS request failed' }) + ); + const request: DeleteDatabaseRequest = { + delbackup: 'y', + }; + + await expect( + service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) + ).rejects.toThrow(CmsError); + }); + + it('should throw DatabaseError if CMS token error occurs', async () => { + (common.checkCmsTokenError as jest.Mock).mockImplementation(() => { + throw DatabaseError.InvalidParameter('Invalid CMS token'); + }); + cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); + const request: DeleteDatabaseRequest = { + delbackup: 'y', + }; + + await expect( + service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) + ).rejects.toThrow(DatabaseError); + }); + + it('should throw DatabaseError if CMS status is fail', async () => { + (common.checkCmsStatusError as jest.Mock).mockImplementation(() => { + throw DatabaseError.InvalidParameter('CMS status failed'); + }); + cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); + const request: DeleteDatabaseRequest = { + delbackup: 'y', + }; + + await expect( + service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) + ).rejects.toThrow(DatabaseError); + }); + }); }); diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts index 0e2177c..72f4bd9 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts @@ -4,6 +4,8 @@ import { CreateDatabaseWithConfigRequest, CreateDatabaseWithConfigResponse, DatabaseVolumeInfoClientResponse, + DeleteDatabaseRequest, + DeleteDatabaseResponse, StartInfoClientResponse, } from '@api-interfaces'; import { GetCreatedbInfoClientResponse } from '@api-interfaces/response/get-createdb-info-client-response'; @@ -11,8 +13,11 @@ import { CmsConfigService } from '@cms-config/cms-config.service'; import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; import { BaseService, + HandleCmsStatusErrors, HandleDatabaseErrors, } from '@common'; +import { ConfigError } from '@error/config/config-error'; +import { ConfigErrorCode } from '@error/config/config-error-code'; import { DatabaseError } from '@error/database/database-error'; import { HostError } from '@error/index'; import { ValidationError } from '@error/validation/validation-error'; @@ -26,12 +31,14 @@ import { DatabaseConfigService } from '../config/database-config.service'; import { DATABASE_CONSTANTS } from '../database.constants'; import { CreateDatabaseCmsRequest, + DeleteDatabaseCmsRequest, DbSpaceInfoCmsRequest, StartDatabaseCmsRequest, StopDatabaseCmsRequest, } from '@type/cms-request'; import { CreateDatabaseCmsResponse, + DeleteDatabaseCmsResponse, DbSpaceInfoCmsResponse, StartInfoCmsResponse, } from '@type/cms-response'; @@ -608,4 +615,74 @@ export class DatabaseLifecycleService extends BaseService { return response; } + + /** + * Delete a database. + * Also removes the database name from the server parameter in cubridconf if it exists. + * Returns empty object on success. + * + * @param userId User ID from JWT + * @param hostUid Host UID + * @param dbname Database name + * @param request Client request containing delbackup option + * @returns DeleteDatabaseResponse Empty object on success + * @throws DatabaseError If request fails or CMS status is fail + */ + @HandleDatabaseErrors() + @HandleCmsStatusErrors() + async deleteDatabase( + userId: string, + hostUid: string, + dbname: string, + request: DeleteDatabaseRequest + ): Promise { + const cmsRequest: DeleteDatabaseCmsRequest = { + task: 'deletedb', + dbname: dbname, + delbackup: request.delbackup, + }; + + this.logger.debug(`Deleting database: ${dbname} on host: ${hostUid}`); + + await this.executeCmsRequest( + userId, + hostUid, + cmsRequest + ); + + // Remove dbname from server parameter in cubridconf if it exists + try { + await this.databaseConfigService.removeAutoStart(userId, hostUid, { + confname: DATABASE_CONSTANTS.CUBRID_CONF_NAME, + dbname: dbname, + }); + this.logger.debug( + `Successfully removed database name ${dbname} from server parameter in cubridconf` + ); + } catch (error: unknown) { + // Ignore DbnameNotFound error (dbname may not exist in server parameter) + // Log other errors but don't fail the delete operation + if (error instanceof ConfigError && error.code === ConfigErrorCode.DBNAME_NOT_FOUND) { + this.logger.debug( + `Database name ${dbname} not found in server parameter, skipping removal (this is expected if auto-start was not configured)` + ); + } else { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorCode = error instanceof ConfigError ? error.code : 'UNKNOWN'; + this.logger.warn( + `Failed to remove dbname from server parameter during database deletion: ${errorMessage}`, + { + dbname, + hostUid, + errorCode, + stack: errorStack, + } + ); + } + } + + // Success: return empty object + return {}; + } } diff --git a/apps/api-server/src/database/management/database-management.controller.ts b/apps/api-server/src/database/management/database-management.controller.ts index bec38ef..af9f19d 100644 --- a/apps/api-server/src/database/management/database-management.controller.ts +++ b/apps/api-server/src/database/management/database-management.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Logger, Param, Post, Request } from '@nestjs/common'; +import { Body, Controller, Get, Logger, Param, Post, Request } from '@nestjs/common'; import { AddVolDbRequest, AddVolDbResponse, @@ -6,8 +6,6 @@ import { CheckDatabaseResponse, CompactDatabaseRequest, CompactDatabaseResponse, - DeleteDatabaseRequest, - DeleteDatabaseResponse, GetAddVolStatusResponse, LoadDatabaseRequest, LoadDatabaseResponse, @@ -441,35 +439,4 @@ export class DatabaseManagementController { return await this.managementService.killTransaction(userId, hostUid, dbname, body); } - /** - * Delete a database. - * Also removes the database name from the server parameter in cubridconf if it exists. - * Returns empty object on success. - * - * @route DELETE /:hostUid/database/:dbname - * @param req Express request (contains authenticated user) - * @param hostUid Host unique identifier from path parameter - * @param dbname Database name from path parameter - * @param body Request body containing delbackup option - * @returns DeleteDatabaseResponse Empty object on success - * @example - * // DELETE /host-uid/database/test - * // Body: { "delbackup": "y" } - */ - @Delete(':dbname') - async deleteDatabase( - @Request() req, - @Param('hostUid') hostUid: string, - @Param('dbname') dbname: string, - @Body() body: DeleteDatabaseRequest - ): Promise { - const userId = req.user.sub; - - validateRequiredFields(body, ['delbackup'], 'database/delete', this.logger); - - this.logger.log( - `Deleting database: ${dbname} on host: ${hostUid}` - ); - return await this.managementService.deleteDatabase(userId, hostUid, dbname, body); - } } diff --git a/apps/api-server/src/database/management/database-management.service.spec.ts b/apps/api-server/src/database/management/database-management.service.spec.ts index a5ab7b9..d59b126 100644 --- a/apps/api-server/src/database/management/database-management.service.spec.ts +++ b/apps/api-server/src/database/management/database-management.service.spec.ts @@ -14,7 +14,6 @@ import { LockDatabaseRequest, GetTransactionInfoRequest, KillTransactionRequest, - DeleteDatabaseRequest, } from '@api-interfaces'; import { UnloadDatabaseCmsResponse, @@ -25,7 +24,6 @@ import { LockDatabaseCmsResponse, GetTransactionInfoCmsResponse, KillTransactionCmsResponse, - DeleteDatabaseCmsResponse, } from '@type/cms-response'; import * as common from '@common'; @@ -1852,115 +1850,4 @@ describe('DatabaseManagementService', () => { }); }); - describe('deleteDatabase', () => { - const mockSuccessResponse: DeleteDatabaseCmsResponse = { - __EXEC_TIME: '848 ms', - note: 'none', - status: 'success', - task: 'deletedb', - }; - - it('should successfully delete database with delbackup "y"', async () => { - cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); - const request: DeleteDatabaseRequest = { - delbackup: 'y', - }; - - const result = await service.deleteDatabase( - mockUserId, - mockHostUid, - mockDbname, - request - ); - - expect(hostService.findHostInternal).toHaveBeenCalledWith(mockUserId, mockHostUid); - expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( - `https://${mockHost.address}:${mockHost.port}/cm_api`, - { - task: 'deletedb', - token: mockHost.token, - dbname: mockDbname, - delbackup: 'y', - } - ); - expect(result).toEqual({}); - }); - - it('should successfully delete database with delbackup "n"', async () => { - cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); - const request: DeleteDatabaseRequest = { - delbackup: 'n', - }; - - const result = await service.deleteDatabase( - mockUserId, - mockHostUid, - mockDbname, - request - ); - - expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - task: 'deletedb', - delbackup: 'n', - }) - ); - expect(result).toEqual({}); - }); - - it('should throw HostError if host is not found', async () => { - hostService.findHostInternal.mockRejectedValue( - HostError.NoSuchHost({ hostUid: mockHostUid }) - ); - const request: DeleteDatabaseRequest = { - delbackup: 'y', - }; - - await expect( - service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) - ).rejects.toThrow(HostError); - }); - - it('should throw CmsError if CMS request fails', async () => { - cmsClient.postAuthenticated.mockRejectedValue( - CmsError.RequestFailed({ message: 'CMS request failed' }) - ); - const request: DeleteDatabaseRequest = { - delbackup: 'y', - }; - - await expect( - service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) - ).rejects.toThrow(CmsError); - }); - - it('should throw DatabaseError if CMS token error occurs', async () => { - (common.checkCmsTokenError as jest.Mock).mockImplementation(() => { - throw DatabaseError.InvalidParameter('Invalid CMS token'); - }); - cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); - const request: DeleteDatabaseRequest = { - delbackup: 'y', - }; - - await expect( - service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) - ).rejects.toThrow(DatabaseError); - }); - - it('should throw DatabaseError if CMS status is fail', async () => { - (common.checkCmsStatusError as jest.Mock).mockImplementation(() => { - throw DatabaseError.InvalidParameter('CMS status failed'); - }); - cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); - const request: DeleteDatabaseRequest = { - delbackup: 'y', - }; - - await expect( - service.deleteDatabase(mockUserId, mockHostUid, mockDbname, request) - ).rejects.toThrow(DatabaseError); - }); - }); }); diff --git a/apps/api-server/src/database/management/database-management.service.ts b/apps/api-server/src/database/management/database-management.service.ts index e5aa882..4cc8de6 100644 --- a/apps/api-server/src/database/management/database-management.service.ts +++ b/apps/api-server/src/database/management/database-management.service.ts @@ -5,8 +5,6 @@ import { CheckDatabaseResponse, CompactDatabaseRequest, CompactDatabaseResponse, - DeleteDatabaseRequest, - DeleteDatabaseResponse, GetAddVolStatusResponse, LoadDatabaseRequest, LoadDatabaseResponse, @@ -32,8 +30,6 @@ import { HandleCmsStatusErrors, HandleDatabaseErrors, } from '@common'; -import { ConfigError } from '@error/config/config-error'; -import { ConfigErrorCode } from '@error/config/config-error-code'; import { CmsError } from '@error/cms/cms-error'; import { DatabaseError } from '@error/database/database-error'; import { HostService } from '@host'; @@ -44,7 +40,6 @@ import { AddVolDbCmsRequest, CheckDatabaseCmsRequest, CompactDatabaseCmsRequest, - DeleteDatabaseCmsRequest, GetAddVolStatusCmsRequest, LoadDatabaseCmsRequest, LockDatabaseCmsRequest, @@ -59,7 +54,6 @@ import { AddVolDbCmsResponse, CheckDatabaseCmsResponse, CompactDatabaseCmsResponse, - DeleteDatabaseCmsResponse, GetAddVolStatusCmsResponse, LoadDatabaseCmsResponse, LockDatabaseCmsResponse, @@ -629,74 +623,4 @@ export class DatabaseManagementService extends BaseService { return this.extractDomainData(response); } - - /** - * Delete a database. - * Also removes the database name from the server parameter in cubridconf if it exists. - * Returns empty object on success. - * - * @param userId User ID from JWT - * @param hostUid Host UID - * @param dbname Database name - * @param request Client request containing delbackup option - * @returns DeleteDatabaseResponse Empty object on success - * @throws DatabaseError If request fails or CMS status is fail - */ - @HandleDatabaseErrors() - @HandleCmsStatusErrors() - async deleteDatabase( - userId: string, - hostUid: string, - dbname: string, - request: DeleteDatabaseRequest - ): Promise { - const cmsRequest: DeleteDatabaseCmsRequest = { - task: 'deletedb', - dbname: dbname, - delbackup: request.delbackup, - }; - - this.logger.debug(`Deleting database: ${dbname} on host: ${hostUid}`); - - await this.executeCmsRequest( - userId, - hostUid, - cmsRequest - ); - - // Remove dbname from server parameter in cubridconf if it exists - try { - await this.databaseConfigService.removeAutoStart(userId, hostUid, { - confname: DATABASE_CONSTANTS.CUBRID_CONF_NAME, - dbname: dbname, - }); - this.logger.debug( - `Successfully removed database name ${dbname} from server parameter in cubridconf` - ); - } catch (error: unknown) { - // Ignore DbnameNotFound error (dbname may not exist in server parameter) - // Log other errors but don't fail the delete operation - if (error instanceof ConfigError && error.code === ConfigErrorCode.DBNAME_NOT_FOUND) { - this.logger.debug( - `Database name ${dbname} not found in server parameter, skipping removal (this is expected if auto-start was not configured)` - ); - } else { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorStack = error instanceof Error ? error.stack : undefined; - const errorCode = error instanceof ConfigError ? error.code : 'UNKNOWN'; - this.logger.warn( - `Failed to remove dbname from server parameter during database deletion: ${errorMessage}`, - { - dbname, - hostUid, - errorCode, - stack: errorStack, - } - ); - } - } - - // Success: return empty object - return {}; - } } From ea6526d03b36d05cd876c851028c517b57232d10 Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Mon, 9 Mar 2026 10:49:12 +0900 Subject: [PATCH 09/11] refactoring : start-info is moved into new module database-info refactoring : modules those import start-info now doesn't import entire DatabaseLifeCycle Module, instead it implements DatabaseInfo module --- .../src/database/database.module.ts | 10 +- .../src/database/info/database-info.module.ts | 12 ++ .../info/database-info.service.spec.ts | 142 ++++++++++++++++++ .../database/info/database-info.service.ts | 103 +++++++++++++ apps/api-server/src/database/info/index.ts | 2 + .../database-lifecycle.controller.ts | 6 +- .../database-lifecycle.service.spec.ts | 36 ++++- .../lifecycle/database-lifecycle.service.ts | 115 +++----------- .../database-management.controller.ts | 6 +- .../database-management.service.spec.ts | 16 +- .../management/database-management.service.ts | 18 +-- 11 files changed, 344 insertions(+), 122 deletions(-) create mode 100644 apps/api-server/src/database/info/database-info.module.ts create mode 100644 apps/api-server/src/database/info/database-info.service.spec.ts create mode 100644 apps/api-server/src/database/info/database-info.service.ts create mode 100644 apps/api-server/src/database/info/index.ts diff --git a/apps/api-server/src/database/database.module.ts b/apps/api-server/src/database/database.module.ts index 3be002e..f2c8a6e 100644 --- a/apps/api-server/src/database/database.module.ts +++ b/apps/api-server/src/database/database.module.ts @@ -8,6 +8,7 @@ import { DatabaseUserController } from './user/database-user.controller'; import { DatabaseUserService } from './user/database-user.service'; import { CmsConfigModule } from '@cms-config/cms-config.module'; import { FileModule } from '@file/file.module'; +import { DatabaseInfoModule } from './info/database-info.module'; import { DatabaseLifecycleController } from './lifecycle/database-lifecycle.controller'; import { DatabaseLifecycleService } from './lifecycle/database-lifecycle.service'; import { DatabaseBackupController } from './backup/database-backup.controller'; @@ -41,6 +42,13 @@ import { DatabaseConfigService } from './config/database-config.service'; DatabaseManagementService, DatabaseConfigService, ], - imports: [HostModule, CmsHttpsClientModule, UserRepositoryModule, CmsConfigModule, FileModule], + imports: [ + HostModule, + CmsHttpsClientModule, + UserRepositoryModule, + CmsConfigModule, + FileModule, + DatabaseInfoModule, + ], }) export class DatabaseModule {} diff --git a/apps/api-server/src/database/info/database-info.module.ts b/apps/api-server/src/database/info/database-info.module.ts new file mode 100644 index 0000000..762faee --- /dev/null +++ b/apps/api-server/src/database/info/database-info.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { HostModule } from '@host'; +import { CmsHttpsClientModule } from '@cms-https-client/cms-https-client.module'; +import { CmsConfigModule } from '@cms-config/cms-config.module'; +import { DatabaseInfoService } from './database-info.service'; + +@Module({ + imports: [HostModule, CmsHttpsClientModule, CmsConfigModule], + providers: [DatabaseInfoService], + exports: [DatabaseInfoService], +}) +export class DatabaseInfoModule {} diff --git a/apps/api-server/src/database/info/database-info.service.spec.ts b/apps/api-server/src/database/info/database-info.service.spec.ts new file mode 100644 index 0000000..f6765b8 --- /dev/null +++ b/apps/api-server/src/database/info/database-info.service.spec.ts @@ -0,0 +1,142 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DatabaseInfoService } from './database-info.service'; +import { HostService } from '@host'; +import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; +import { CmsConfigService } from '@cms-config/cms-config.service'; +import { DatabaseError } from '@error/database/database-error'; +import * as common from '@common'; + +jest.mock('@common', () => ({ + ...jest.requireActual('@common'), + checkCmsTokenError: jest.fn(), + checkCmsStatusError: jest.fn(), +})); + +describe('DatabaseInfoService', () => { + let service: DatabaseInfoService; + let hostService: jest.Mocked; + let cmsClient: jest.Mocked; + let cmsConfigService: jest.Mocked; + + const mockHost = { + uid: 'host-uid-1', + id: 'host-1', + address: 'localhost', + port: 8001, + password: 'host-password', + token: 'test-token', + dbProfiles: {}, + }; + + const mockUserId = 'user-123'; + const mockHostUid = 'host-uid-1'; + + beforeEach(async () => { + const mockHostService = { findHostInternal: jest.fn() }; + const mockCmsClient = { postAuthenticated: jest.fn() }; + const mockCmsConfigService = { getEnv: jest.fn() }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DatabaseInfoService, + { provide: HostService, useValue: mockHostService }, + { provide: CmsHttpsClientService, useValue: mockCmsClient }, + { provide: CmsConfigService, useValue: mockCmsConfigService }, + ], + }).compile(); + + service = module.get(DatabaseInfoService); + hostService = module.get(HostService); + cmsClient = module.get(CmsHttpsClientService); + cmsConfigService = module.get(CmsConfigService); + + hostService.findHostInternal.mockResolvedValue(mockHost); + (common.checkCmsTokenError as jest.Mock).mockImplementation(() => {}); + (common.checkCmsStatusError as jest.Mock).mockImplementation(() => {}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('startInfoInternal', () => { + it('should return raw CMS startinfo response', async () => { + const mockResponse = { + __EXEC_TIME: '10 ms', + note: 'none', + status: 'success', + task: 'startinfo', + dblist: [{ dbs: [{ dbname: 'testdb' }] }], + activelist: [{ active: [] }], + }; + cmsClient.postAuthenticated.mockResolvedValue(mockResponse); + + const result = await service.startInfoInternal(mockUserId, mockHostUid); + + expect(hostService.findHostInternal).toHaveBeenCalledWith(mockUserId, mockHostUid); + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ task: 'startinfo' }) + ); + expect(result).toEqual(mockResponse); + }); + + it('should throw DatabaseError when CMS status is not success', async () => { + cmsClient.postAuthenticated.mockResolvedValue({ + status: 'fail', + task: 'startinfo', + note: 'failed', + }); + + await expect( + service.startInfoInternal(mockUserId, mockHostUid) + ).rejects.toThrow(DatabaseError); + }); + }); + + describe('startInfo', () => { + it('should return client start info with isProfileExists', async () => { + const mockCmsResponse = { + __EXEC_TIME: '10 ms', + note: 'none', + status: 'success', + task: 'startinfo', + dblist: [{ dbs: [{ dbname: 'testdb', dbdir: '/path' }] }], + activelist: [{ active: [{ dbname: 'testdb' }] }], + }; + cmsClient.postAuthenticated.mockResolvedValue(mockCmsResponse); + + const result = await service.startInfo(mockUserId, mockHostUid); + + expect(result).toEqual({ + activelist: { active: [{ dbname: 'testdb' }] }, + dblist: { + dbs: [{ dbname: 'testdb', dbdir: '/path', isProfileExists: false }], + }, + }); + }); + }); + + describe('getCreatedbInfo', () => { + it('should return create info from env', async () => { + cmsConfigService.getEnv.mockResolvedValue({ + CUBRID_DATABASES: '/opt/cubrid/databases', + CUBRIDVER: '11.4', + CUBRID: '/opt/cubrid', + }); + + const result = await service.getCreatedbInfo(mockUserId, mockHostUid); + + expect(cmsConfigService.getEnv).toHaveBeenCalledWith(mockUserId, mockHostUid); + expect(result).toEqual({ + defaultDbDirectory: '/opt/cubrid/databases', + cubridVersion: '11.4', + cubridPath: '/opt/cubrid', + }); + }); + }); +}); diff --git a/apps/api-server/src/database/info/database-info.service.ts b/apps/api-server/src/database/info/database-info.service.ts new file mode 100644 index 0000000..ee19b75 --- /dev/null +++ b/apps/api-server/src/database/info/database-info.service.ts @@ -0,0 +1,103 @@ +import { StartInfoClientResponse } from '@api-interfaces'; +import { GetCreatedbInfoClientResponse } from '@api-interfaces/response/get-createdb-info-client-response'; +import { CmsConfigService } from '@cms-config/cms-config.service'; +import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; +import { BaseService, HandleDatabaseErrors } from '@common'; +import { DatabaseError } from '@error/database/database-error'; +import { HostService } from '@host'; +import { Injectable } from '@nestjs/common'; +import { BaseCmsRequest, BaseCmsResponse } from '@type'; +import { StartInfoCmsResponse } from '@type/cms-response'; + +/** + * Service for database information (read-only) used across database modules. + * Provides start-info, create-info and raw CMS startinfo to avoid circular dependencies. + * + * @category Business Services + * @since 1.0.0 + */ +@Injectable() +export class DatabaseInfoService extends BaseService { + constructor( + protected readonly hostService: HostService, + protected readonly cmsClient: CmsHttpsClientService, + private readonly cmsConfigService: CmsConfigService + ) { + super(hostService, cmsClient); + } + + /** + * Get start information for databases on a host (internal/raw CMS). + * Returns raw CMS response without transformation. + * + * @param userId User ID from JWT + * @param hostUid Host UID + * @returns StartInfoCmsResponse + * @throws DatabaseError If request fails or CMS status is fail + */ + @HandleDatabaseErrors() + async startInfoInternal(userId: string, hostUid: string): Promise { + const cmsRequest: BaseCmsRequest = { + task: 'startinfo', + }; + const response = await this.executeCmsRequest< + BaseCmsRequest, + StartInfoCmsResponse | BaseCmsResponse + >(userId, hostUid, cmsRequest); + + if (response.status === 'success') { + return response as StartInfoCmsResponse; + } else { + throw DatabaseError.GetStartInfoFailed({ response }); + } + } + + /** + * Get start information for databases on a host (client shape with isProfileExists). + * + * @param userId User ID from JWT + * @param hostUid Host UID + * @returns StartInfoClientResponse + * @throws DatabaseError If request fails or CMS status is fail + */ + @HandleDatabaseErrors() + async startInfo(userId: string, hostUid: string): Promise { + const host = await this.hostService.findHostInternal(userId, hostUid); + const response = await this.startInfoInternal(userId, hostUid); + const dataOnly = this.extractDomainData(response); + const dbProfiles = host.dbProfiles || {}; + const dbs = dataOnly.dblist?.[0]?.dbs || []; + const activeList = dataOnly.activelist?.[0]?.active || []; + + const clientResponse: StartInfoClientResponse = { + activelist: { active: activeList }, + dblist: { + dbs: dbs.map((db) => ({ + ...db, + isProfileExists: !!dbProfiles[db.dbname], + })), + }, + }; + + return clientResponse; + } + + /** + * Get default information for creating a database. + * + * @param userId User ID from JWT + * @param hostUid Host UID + * @returns GetCreatedbInfoClientResponse + * @throws DatabaseError If request fails + */ + @HandleDatabaseErrors() + async getCreatedbInfo(userId: string, hostUid: string): Promise { + const envInfo = await this.cmsConfigService.getEnv(userId, hostUid); + + return { + defaultDbDirectory: envInfo.CUBRID_DATABASES || '', + cubridVersion: envInfo.CUBRIDVER, + cubridPath: envInfo.CUBRID, + }; + } +} diff --git a/apps/api-server/src/database/info/index.ts b/apps/api-server/src/database/info/index.ts new file mode 100644 index 0000000..c6c7b51 --- /dev/null +++ b/apps/api-server/src/database/info/index.ts @@ -0,0 +1,2 @@ +export * from './database-info.service'; +export * from './database-info.module'; diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts index d271ad0..79069e0 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.controller.ts @@ -6,7 +6,6 @@ import { CreateDatabaseWithConfigResponse, DatabaseVolumeInfoClientResponse, DeleteDatabaseRequest, - DeleteDatabaseResponse, GetCreatedbInfoClientResponse, StartInfoClientResponse, SaveDatabaseProfileRequest, @@ -262,13 +261,14 @@ export class DatabaseLifecycleController { /** * Delete a database on a host. * Also removes the database name from the server parameter in cubridconf if it exists. + * Returns start-info (db list) on success. * * @route DELETE /:hostUid/database/:dbname * @param req Express request (contains authenticated user) * @param hostUid Host unique identifier from path parameter * @param dbname Database name from path parameter * @param body Request body containing delbackup option - * @returns DeleteDatabaseResponse Empty object on success + * @returns StartInfoClientResponse Latest database list (start-info) on success * @example * // DELETE /host-uid/database/testdb * // Body: { "delbackup": "y" } @@ -279,7 +279,7 @@ export class DatabaseLifecycleController { @Param('hostUid') hostUid: string, @Param('dbname') dbname: string, @Body() body: DeleteDatabaseRequest - ): Promise { + ): Promise { const userId = req.user.sub; validateRequiredFields(body, ['delbackup'], 'database/delete', this.logger); diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts index ba9cc48..dc5d02b 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.service.spec.ts @@ -5,6 +5,7 @@ import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.servic import { UserRepositoryService } from '@repository'; import { CmsConfigService } from '@cms-config/cms-config.service'; import { FileService } from '@file/file.service'; +import { DatabaseInfoService } from '../info/database-info.service'; import { DatabaseUserService } from '../user/database-user.service'; import { DatabaseConfigService } from '../config/database-config.service'; import { DatabaseError } from '@error/database/database-error'; @@ -30,6 +31,7 @@ describe('DatabaseLifecycleService', () => { let fileService: jest.Mocked; let databaseUserService: jest.Mocked; let databaseConfigService: jest.Mocked; + let databaseInfoService: DatabaseInfoService; const mockHost = { uid: 'host-uid-1', @@ -73,6 +75,7 @@ describe('DatabaseLifecycleService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ DatabaseLifecycleService, + DatabaseInfoService, { provide: HostService, useValue: mockHostService, @@ -112,6 +115,7 @@ describe('DatabaseLifecycleService', () => { fileService = module.get(FileService); databaseUserService = module.get(DatabaseUserService); databaseConfigService = module.get(DatabaseConfigService); + databaseInfoService = module.get(DatabaseInfoService); // Setup default mocks hostService.findHostInternal.mockResolvedValue(mockHost); @@ -223,7 +227,7 @@ describe('DatabaseLifecycleService', () => { }; beforeEach(() => { - jest.spyOn(service, 'startInfo').mockResolvedValue(mockStartInfoResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoResponse as any); }); it('should successfully start database', async () => { @@ -271,7 +275,7 @@ describe('DatabaseLifecycleService', () => { }; beforeEach(() => { - jest.spyOn(service, 'startInfo').mockResolvedValue(mockStartInfoResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoResponse as any); }); it('should successfully stop database', async () => { @@ -325,7 +329,7 @@ describe('DatabaseLifecycleService', () => { }; beforeEach(() => { - jest.spyOn(service, 'startInfo').mockResolvedValue(mockStartInfoResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoResponse as any); }); it('should successfully restart database', async () => { @@ -375,7 +379,7 @@ describe('DatabaseLifecycleService', () => { }; beforeEach(() => { - jest.spyOn(service, 'startInfo').mockResolvedValue(mockStartInfoResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoResponse as any); }); it('should successfully save database profile', async () => { @@ -511,8 +515,17 @@ describe('DatabaseLifecycleService', () => { const mockCreateDatabaseResponse: CreateDatabaseClientResponse = {}; + const mockStartInfoForCreate = { activelist: { active: [] }, dblist: { dbs: [] } }; + beforeEach(() => { jest.spyOn(service, 'createDatabaseInternal').mockResolvedValue(mockCreateDatabaseResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoForCreate as any); + cmsClient.postAuthenticated.mockResolvedValue({ + __EXEC_TIME: '0 ms', + note: 'none', + status: 'success', + task: 'startdb', + }); databaseUserService.updateUser.mockResolvedValue({}); databaseConfigService.setAutoAddVol.mockResolvedValue({}); databaseConfigService.setAutoStart.mockResolvedValue({}); @@ -787,8 +800,14 @@ describe('DatabaseLifecycleService', () => { task: 'deletedb', }; - it('should successfully delete database with delbackup "y"', async () => { + const mockStartInfoAfterDelete = { + activelist: { active: [] }, + dblist: { dbs: [] }, + }; + + it('should successfully delete database with delbackup "y" and return start-info', async () => { cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoAfterDelete as any); const request: DeleteDatabaseRequest = { delbackup: 'y', }; @@ -810,11 +829,12 @@ describe('DatabaseLifecycleService', () => { delbackup: 'y', } ); - expect(result).toEqual({}); + expect(result).toEqual(mockStartInfoAfterDelete); }); - it('should successfully delete database with delbackup "n"', async () => { + it('should successfully delete database with delbackup "n" and return start-info', async () => { cmsClient.postAuthenticated.mockResolvedValue(mockSuccessResponse); + jest.spyOn(databaseInfoService, 'startInfo').mockResolvedValue(mockStartInfoAfterDelete as any); const request: DeleteDatabaseRequest = { delbackup: 'n', }; @@ -833,7 +853,7 @@ describe('DatabaseLifecycleService', () => { delbackup: 'n', }) ); - expect(result).toEqual({}); + expect(result).toEqual(mockStartInfoAfterDelete); }); it('should throw HostError if host is not found', async () => { diff --git a/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts b/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts index 72f4bd9..d54a698 100644 --- a/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts +++ b/apps/api-server/src/database/lifecycle/database-lifecycle.service.ts @@ -5,7 +5,6 @@ import { CreateDatabaseWithConfigResponse, DatabaseVolumeInfoClientResponse, DeleteDatabaseRequest, - DeleteDatabaseResponse, StartInfoClientResponse, } from '@api-interfaces'; import { GetCreatedbInfoClientResponse } from '@api-interfaces/response/get-createdb-info-client-response'; @@ -25,7 +24,8 @@ import { FileService } from '@file/file.service'; import { HostService } from '@host'; import { Injectable } from '@nestjs/common'; import { UserRepositoryService } from '@repository'; -import { BaseCmsRequest, BaseCmsResponse } from '@type'; +import { BaseCmsResponse } from '@type'; +import { DatabaseInfoService } from '../info/database-info.service'; import { DatabaseUserService } from '../user/database-user.service'; import { DatabaseConfigService } from '../config/database-config.service'; import { DATABASE_CONSTANTS } from '../database.constants'; @@ -40,7 +40,6 @@ import { CreateDatabaseCmsResponse, DeleteDatabaseCmsResponse, DbSpaceInfoCmsResponse, - StartInfoCmsResponse, } from '@type/cms-response'; import { convertExvolArrayToCmsFormat } from '@util'; @@ -60,67 +59,20 @@ export class DatabaseLifecycleService extends BaseService { private readonly cmsConfigService: CmsConfigService, private readonly fileService: FileService, private readonly databaseUserService: DatabaseUserService, - private readonly databaseConfigService: DatabaseConfigService + private readonly databaseConfigService: DatabaseConfigService, + private readonly databaseInfoService: DatabaseInfoService ) { super(hostService, cmsClient); } - /** - * Get start information for databases on a host (internal use). - * Returns raw CMS response without transformation. - * - * @internal - * @param userId User ID from JWT - * @param hostUid Host UID - * @returns StartInfoCmsResponse - * @throws DatabaseError If request fails or CMS status is fail - */ - @HandleDatabaseErrors() - async startInfoInternal(userId: string, hostUid: string): Promise { - const cmsRequest: BaseCmsRequest = { - task: 'startinfo', - }; - const response = await this.executeCmsRequest< - BaseCmsRequest, - StartInfoCmsResponse | BaseCmsResponse - >(userId, hostUid, cmsRequest); - - if (response.status === 'success') { - return response as StartInfoCmsResponse; - } else { - throw DatabaseError.GetStartInfoFailed({ response }); - } - } - - /** - * Get start information for databases on a host. - * Returns domain-only data (CMS envelope removed). - * - * @param userId User ID from JWT - * @param hostUid Host UID - * @returns StartInfoClientResponse - * @throws DatabaseError If request fails or CMS status is fail - */ - @HandleDatabaseErrors() + /** Delegates to DatabaseInfoService. */ async startInfo(userId: string, hostUid: string): Promise { - const host = await this.hostService.findHostInternal(userId, hostUid); - const response = await this.startInfoInternal(userId, hostUid); - const dataOnly = this.extractDomainData(response); - const dbProfiles = host.dbProfiles || {}; - const dbs = dataOnly.dblist?.[0]?.dbs || []; - const activeList = dataOnly.activelist?.[0]?.active || []; - - const clientResponse: StartInfoClientResponse = { - activelist: { active: activeList }, - dblist: { - dbs: dbs.map((db) => ({ - ...db, - isProfileExists: !!dbProfiles[db.dbname], - })), - }, - }; + return this.databaseInfoService.startInfo(userId, hostUid); + } - return clientResponse; + /** Delegates to DatabaseInfoService. */ + async getCreatedbInfo(userId: string, hostUid: string): Promise { + return this.databaseInfoService.getCreatedbInfo(userId, hostUid); } /** @@ -150,7 +102,7 @@ export class DatabaseLifecycleService extends BaseService { ); if (response.status === 'success') { - return await this.startInfo(userId, hostUid); + return await this.databaseInfoService.startInfo(userId, hostUid); } throw DatabaseError.StartDatabaseFailed({ response, dbname }); @@ -183,7 +135,7 @@ export class DatabaseLifecycleService extends BaseService { ); if (response.status === 'success') { - return await this.startInfo(userId, hostUid); + return await this.databaseInfoService.startInfo(userId, hostUid); } throw DatabaseError.StopDatabaseFailed({ response, dbname }); @@ -227,7 +179,7 @@ export class DatabaseLifecycleService extends BaseService { >(userId, hostUid, startRequest); if (startResponse.status === 'success') { - return await this.startInfo(userId, hostUid); + return await this.databaseInfoService.startInfo(userId, hostUid); } else { throw DatabaseError.StartDatabaseFailed({ response: startResponse, @@ -297,7 +249,7 @@ export class DatabaseLifecycleService extends BaseService { return user; }); - return await this.startInfo(userId, hostUid); + return await this.databaseInfoService.startInfo(userId, hostUid); } /** @@ -316,14 +268,7 @@ export class DatabaseLifecycleService extends BaseService { hostUid: string, dbname: string ): Promise { - const startInfoRequest: BaseCmsRequest = { - task: 'startinfo', - }; - - const startInfo = await this.executeCmsRequest< - BaseCmsRequest, - StartInfoCmsResponse | BaseCmsResponse - >(userId, hostUid, startInfoRequest); + const startInfo = await this.databaseInfoService.startInfoInternal(userId, hostUid); if ('dblist' in startInfo && 'activelist' in startInfo) { const dbExists = startInfo.dblist.some((el) => el.dbs.some((db) => db.dbname === dbname)); @@ -351,26 +296,6 @@ export class DatabaseLifecycleService extends BaseService { } } - /** - * Get default information for creating a database. - * Returns default database directory path and related information. - * - * @param userId User ID from JWT - * @param hostUid Host UID - * @returns GetCreatedbInfoClientResponse Default database creation information - * @throws DatabaseError If request fails - */ - @HandleDatabaseErrors() - async getCreatedbInfo(userId: string, hostUid: string): Promise { - const envInfo = await this.cmsConfigService.getEnv(userId, hostUid); - - return { - defaultDbDirectory: envInfo.CUBRID_DATABASES || '', - cubridVersion: envInfo.CUBRIDVER, - cubridPath: envInfo.CUBRID, - }; - } - /** * Create a new database (internal use). * Returns empty object on success. @@ -619,13 +544,13 @@ export class DatabaseLifecycleService extends BaseService { /** * Delete a database. * Also removes the database name from the server parameter in cubridconf if it exists. - * Returns empty object on success. + * Returns start-info (db list) on success. * * @param userId User ID from JWT * @param hostUid Host UID * @param dbname Database name * @param request Client request containing delbackup option - * @returns DeleteDatabaseResponse Empty object on success + * @returns StartInfoClientResponse Latest database list (start-info) on success * @throws DatabaseError If request fails or CMS status is fail */ @HandleDatabaseErrors() @@ -635,7 +560,7 @@ export class DatabaseLifecycleService extends BaseService { hostUid: string, dbname: string, request: DeleteDatabaseRequest - ): Promise { + ): Promise { const cmsRequest: DeleteDatabaseCmsRequest = { task: 'deletedb', dbname: dbname, @@ -682,7 +607,7 @@ export class DatabaseLifecycleService extends BaseService { } } - // Success: return empty object - return {}; + // Return latest db list (start-info) + return await this.databaseInfoService.startInfo(userId, hostUid); } } diff --git a/apps/api-server/src/database/management/database-management.controller.ts b/apps/api-server/src/database/management/database-management.controller.ts index af9f19d..6bf9c8d 100644 --- a/apps/api-server/src/database/management/database-management.controller.ts +++ b/apps/api-server/src/database/management/database-management.controller.ts @@ -18,7 +18,7 @@ import { OptimizeDatabaseRequest, OptimizeDatabaseResponse, RenameDatabaseRequest, - RenameDatabaseResponse, + StartInfoClientResponse, UnloadDatabaseRequest, UnloadInfoClientResponse, } from '@api-interfaces'; @@ -239,7 +239,7 @@ export class DatabaseManagementController { * @param hostUid Host unique identifier from path parameter * @param dbname Current database name from path parameter * @param body Request body containing rename configuration - * @returns RenameDatabaseResponse Empty object on success + * @returns StartInfoClientResponse Latest database list (start-info) on success * @example * // POST /host-uid/database/rename/rename_test * // Body: { "rename": "renamed_db", "exvolpath": "none", "advanced": "on", "volume": [{ "/old/path": "/new/path" }], "forcedel": "n" } @@ -250,7 +250,7 @@ export class DatabaseManagementController { @Param('hostUid') hostUid: string, @Param('dbname') dbname: string, @Body() body: RenameDatabaseRequest - ): Promise { + ): Promise { const userId = req.user.sub; validateRequiredFields( diff --git a/apps/api-server/src/database/management/database-management.service.spec.ts b/apps/api-server/src/database/management/database-management.service.spec.ts index d59b126..e7be66a 100644 --- a/apps/api-server/src/database/management/database-management.service.spec.ts +++ b/apps/api-server/src/database/management/database-management.service.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DatabaseManagementService } from './database-management.service'; import { HostService } from '@host'; import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; +import { DatabaseInfoService } from '@database/info/database-info.service'; import { DatabaseError } from '@error/database/database-error'; import { HostError } from '@error/index'; import { CmsError } from '@error/cms/cms-error'; @@ -62,6 +63,11 @@ describe('DatabaseManagementService', () => { postAuthenticated: jest.fn(), }; + const mockStartInfoResponse = { activelist: { active: [] }, dblist: { dbs: [] } }; + const mockDatabaseInfoService = { + startInfo: jest.fn().mockResolvedValue(mockStartInfoResponse), + }; + const module: TestingModule = await Test.createTestingModule({ providers: [ DatabaseManagementService, @@ -73,6 +79,10 @@ describe('DatabaseManagementService', () => { provide: CmsHttpsClientService, useValue: mockCmsClient, }, + { + provide: DatabaseInfoService, + useValue: mockDatabaseInfoService, + }, ], }).compile(); @@ -937,7 +947,7 @@ describe('DatabaseManagementService', () => { ); expect(common.checkCmsTokenError).toHaveBeenCalledWith(mockSuccessResponse); expect(common.checkCmsStatusError).toHaveBeenCalledWith(mockSuccessResponse); - expect(result).toEqual({}); + expect(result).toEqual(mockStartInfoResponse); }); it('should successfully rename database with advanced "off" without volume', async () => { @@ -969,7 +979,7 @@ describe('DatabaseManagementService', () => { volume: expect.anything(), }) ); - expect(result).toEqual({}); + expect(result).toEqual(mockStartInfoResponse); }); it('should not include volume in CMS request when advanced is "off" even if volume is provided', async () => { @@ -992,7 +1002,7 @@ describe('DatabaseManagementService', () => { // Volume should not be included when advanced is 'off' const callArgs = cmsClient.postAuthenticated.mock.calls[0][1] as any; expect(callArgs.volume).toBeUndefined(); - expect(result).toEqual({}); + expect(result).toEqual(mockStartInfoResponse); }); it('should throw HostError if host is not found', async () => { diff --git a/apps/api-server/src/database/management/database-management.service.ts b/apps/api-server/src/database/management/database-management.service.ts index 4cc8de6..6795fc5 100644 --- a/apps/api-server/src/database/management/database-management.service.ts +++ b/apps/api-server/src/database/management/database-management.service.ts @@ -17,13 +17,13 @@ import { OptimizeDatabaseRequest, OptimizeDatabaseResponse, RenameDatabaseRequest, - RenameDatabaseResponse, + StartInfoClientResponse, UnloadDatabaseRequest, UnloadInfoClientResponse, } from '@api-interfaces'; import { CmsConfigService } from '@cms-config/cms-config.service'; import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; -import { DatabaseConfigService } from '@database/config/database-config.service'; +import { DatabaseInfoService } from '@database/info/database-info.service'; import { checkCmsStatusError, checkCmsTokenError, @@ -77,7 +77,7 @@ export class DatabaseManagementService extends BaseService { constructor( hostService: HostService, cmsClient: CmsHttpsClientService, - private readonly databaseConfigService: DatabaseConfigService + private readonly databaseInfoService: DatabaseInfoService ) { super(hostService, cmsClient); } @@ -368,13 +368,13 @@ export class DatabaseManagementService extends BaseService { /** * Rename a database. - * Returns empty object on success. + * Returns start-info (db list) on success. * * @param userId User ID from JWT * @param hostUid Host UID * @param dbname Current database name * @param request Client request containing rename configuration - * @returns RenameDatabaseResponse Empty object on success + * @returns StartInfoClientResponse Latest database list (start-info) on success * @throws DatabaseError If request fails or CMS status is fail */ @HandleDatabaseErrors() @@ -384,7 +384,7 @@ export class DatabaseManagementService extends BaseService { hostUid: string, dbname: string, request: RenameDatabaseRequest - ): Promise { + ): Promise { // Build CMS request from client request const cmsRequest: RenameDatabaseCmsRequest = { task: 'renamedb', @@ -406,13 +406,13 @@ export class DatabaseManagementService extends BaseService { cmsRequest.volume = [volumeMapping]; } - const response = await this.executeCmsRequest< + await this.executeCmsRequest< RenameDatabaseCmsRequest, RenameDatabaseCmsResponse >(userId, hostUid, cmsRequest); - // Success: return empty object - return {}; + // Return latest db list (start-info) + return await this.databaseInfoService.startInfo(userId, hostUid); } /** From 754b38228a8f1a24cb57bdba598a348c5ce8eb7c Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Mon, 9 Mar 2026 10:51:01 +0900 Subject: [PATCH 10/11] fix : set/remove auto-start URI changed --- .../src/database/config/database-config.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api-server/src/database/config/database-config.controller.ts b/apps/api-server/src/database/config/database-config.controller.ts index abe0aaa..7e9e417 100644 --- a/apps/api-server/src/database/config/database-config.controller.ts +++ b/apps/api-server/src/database/config/database-config.controller.ts @@ -104,7 +104,7 @@ export class DatabaseConfigController { * // POST /host-uid/database/auto-start * // Body: { "confname": "cubridconf", "dbname": "testdb" } */ - @Post('auto-start') + @Post('auto-start/set') async setAutoStart( @Request() req, @Param('hostUid') hostUid: string, @@ -131,7 +131,7 @@ export class DatabaseConfigController { * // DELETE /host-uid/database/auto-start * // Body: { "confname": "cubridconf", "dbname": "testdb" } */ - @Delete('auto-start') + @Delete('auto-start/remove') async removeAutoStart( @Request() req, @Param('hostUid') hostUid: string, From d356ac3e668169117e0687ba756c0a40c1d0fa41 Mon Sep 17 00:00:00 2001 From: JohnDohnut Date: Mon, 9 Mar 2026 15:07:20 +0900 Subject: [PATCH 11/11] feature : stop / start all brokers api --- WebCA Server API.postman_collection.json | 64 +++++ .../src/broker/broker.controller.spec.ts | 99 ++++++++ .../src/broker/broker.controller.ts | 123 +++++++++- .../src/broker/broker.service.spec.ts | 228 +++++++++++++++++- apps/api-server/src/broker/broker.service.ts | 134 ++++++++-- .../src/error/broker/broker-error-code.ts | 2 + .../src/error/broker/broker-error.ts | 24 ++ .../cms-request/add-dbmt-user-cms-request.ts | 19 ++ .../cms-request/handle-broker-cms-request.ts | 10 + apps/api-server/src/type/cms-request/index.ts | 4 + .../cms-request/start-broker-cms-request.ts | 9 + .../cms-request/stop-broker-cms-request.ts | 8 +- .../update-dbmt-user-cms-request.ts | 20 ++ .../add-dbmt-user-cms-response.ts | 20 ++ .../api-server/src/type/cms-response/index.ts | 4 + .../cms-response/start-broker-cms-response.ts | 8 + .../cms-response/stop-broker-cms-response.ts | 8 + .../update-dbmt-user-cms-response.ts | 20 ++ .../src/request/add-dbmt-user-request.ts | 15 ++ libs/api-interfaces/src/request/index.ts | 2 + .../src/request/update-dbmt-user-request.ts | 16 ++ .../src/response/add-dbmt-user-response.ts | 17 ++ libs/api-interfaces/src/response/index.ts | 4 + .../response/start-all-brokers-response.ts | 6 + .../src/response/stop-all-brokers-response.ts | 6 + .../src/response/update-dbmt-user-response.ts | 17 ++ 26 files changed, 860 insertions(+), 27 deletions(-) create mode 100644 apps/api-server/src/type/cms-request/add-dbmt-user-cms-request.ts create mode 100644 apps/api-server/src/type/cms-request/handle-broker-cms-request.ts create mode 100644 apps/api-server/src/type/cms-request/start-broker-cms-request.ts create mode 100644 apps/api-server/src/type/cms-request/update-dbmt-user-cms-request.ts create mode 100644 apps/api-server/src/type/cms-response/add-dbmt-user-cms-response.ts create mode 100644 apps/api-server/src/type/cms-response/start-broker-cms-response.ts create mode 100644 apps/api-server/src/type/cms-response/stop-broker-cms-response.ts create mode 100644 apps/api-server/src/type/cms-response/update-dbmt-user-cms-response.ts create mode 100644 libs/api-interfaces/src/request/add-dbmt-user-request.ts create mode 100644 libs/api-interfaces/src/request/update-dbmt-user-request.ts create mode 100644 libs/api-interfaces/src/response/add-dbmt-user-response.ts create mode 100644 libs/api-interfaces/src/response/start-all-brokers-response.ts create mode 100644 libs/api-interfaces/src/response/stop-all-brokers-response.ts create mode 100644 libs/api-interfaces/src/response/update-dbmt-user-response.ts diff --git a/WebCA Server API.postman_collection.json b/WebCA Server API.postman_collection.json index c9679b6..7bdfffe 100644 --- a/WebCA Server API.postman_collection.json +++ b/WebCA Server API.postman_collection.json @@ -1225,6 +1225,70 @@ { "name": "Broker", "item": [ + { + "name": "Start All Brokers", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/{{hostUid}}/broker/start-all", + "host": ["{{base_url}}"], + "path": ["{{hostUid}}", "broker", "start-all"] + }, + "description": "Start all brokers on the host (CMS task: startbroker). POST /:hostUid/broker/start-all. Auth: Bearer JWT." + }, + "response": [] + }, + { + "name": "Stop All Brokers", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/{{hostUid}}/broker/stop-all", + "host": ["{{base_url}}"], + "path": ["{{hostUid}}", "broker", "stop-all"] + }, + "description": "Stop all brokers on the host (CMS task: stopbroker). POST /:hostUid/broker/stop-all. Auth: Bearer JWT." + }, + "response": [] + }, + { + "name": "Add DBMT User", + "request": { + "method": "POST", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\n \"targetid\": \"test_user_2\",\n \"password\": \"1234\",\n \"casauth\": \"none\",\n \"dbcreate\": \"none\",\n \"statusmonitorauth\": \"none\"\n}" + }, + "url": { + "raw": "{{base_url}}/{{hostUid}}/broker/dbmt-user", + "host": ["{{base_url}}"], + "path": ["{{hostUid}}", "broker", "dbmt-user"] + }, + "description": "Add a DBMT (CMS) user (CMS task: adddbmtuser). POST /:hostUid/broker/dbmt-user. Body: targetid, password, casauth, dbcreate, statusmonitorauth. Auth: Bearer JWT." + }, + "response": [] + }, + { + "name": "Update DBMT User", + "request": { + "method": "PATCH", + "header": [{"key": "Content-Type", "value": "application/json"}], + "body": { + "mode": "raw", + "raw": "{\n \"targetid\": \"test_user_2\",\n \"dbauth\": [],\n \"casauth\": \"none\",\n \"dbcreate\": \"none\",\n \"statusmonitorauth\": \"none\"\n}" + }, + "url": { + "raw": "{{base_url}}/{{hostUid}}/broker/dbmt-user", + "host": ["{{base_url}}"], + "path": ["{{hostUid}}", "broker", "dbmt-user"] + }, + "description": "Update a DBMT (CMS) user (CMS task: updatedbmtuser). PATCH /:hostUid/broker/dbmt-user. Body: targetid, casauth, dbcreate, statusmonitorauth (dbauth optional). Auth: Bearer JWT." + }, + "response": [] + }, { "name": "Get Broker List", "request": { diff --git a/apps/api-server/src/broker/broker.controller.spec.ts b/apps/api-server/src/broker/broker.controller.spec.ts index f99665c..d8746c1 100644 --- a/apps/api-server/src/broker/broker.controller.spec.ts +++ b/apps/api-server/src/broker/broker.controller.spec.ts @@ -1,18 +1,117 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BrokerController } from './broker.controller'; +import { BrokerService } from './broker.service'; describe('BrokerController', () => { let controller: BrokerController; + let brokerService: jest.Mocked; beforeEach(async () => { + const mockBrokerService = { + getBrokers: jest.fn(), + stopBroker: jest.fn(), + startBroker: jest.fn(), + restartBroker: jest.fn(), + getBrokerStatus: jest.fn(), + startAllBrokers: jest.fn(), + stopAllBrokers: jest.fn(), + addDbmtUser: jest.fn(), + updateDbmtUser: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ controllers: [BrokerController], + providers: [ + { + provide: BrokerService, + useValue: mockBrokerService, + }, + ], }).compile(); controller = module.get(BrokerController); + brokerService = module.get(BrokerService); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('startAllBrokers', () => { + it('should call brokerService.startAllBrokers and return { success: true }', async () => { + const req = { user: { sub: 'user-123' } }; + brokerService.startAllBrokers.mockResolvedValue({ success: true }); + + const result = await controller.startAllBrokers(req, 'host-uid-1'); + + expect(brokerService.startAllBrokers).toHaveBeenCalledWith( + 'user-123', + 'host-uid-1' + ); + expect(result).toEqual({ success: true }); + }); + }); + + describe('stopAllBrokers', () => { + it('should call brokerService.stopAllBrokers and return { success: true }', async () => { + const req = { user: { sub: 'user-123' } }; + brokerService.stopAllBrokers.mockResolvedValue({ success: true }); + + const result = await controller.stopAllBrokers(req, 'host-uid-1'); + + expect(brokerService.stopAllBrokers).toHaveBeenCalledWith( + 'user-123', + 'host-uid-1' + ); + expect(result).toEqual({ success: true }); + }); + }); + + describe('addDbmtUser', () => { + it('should call brokerService.addDbmtUser and return dblist and userlist', async () => { + const req = { user: { sub: 'user-123' } }; + const body = { + targetid: 'test_user_2', + password: '1234', + casauth: 'none', + dbcreate: 'none', + statusmonitorauth: 'none', + }; + const mockResponse = { dblist: [], userlist: [] }; + brokerService.addDbmtUser.mockResolvedValue(mockResponse); + + const result = await controller.addDbmtUser(req, 'host-uid-1', body); + + expect(brokerService.addDbmtUser).toHaveBeenCalledWith( + 'user-123', + 'host-uid-1', + body + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('updateDbmtUser', () => { + it('should call brokerService.updateDbmtUser and return dblist and userlist', async () => { + const req = { user: { sub: 'user-123' } }; + const body = { + targetid: 'test_user_2', + dbauth: [], + casauth: 'none', + dbcreate: 'none', + statusmonitorauth: 'none', + }; + const mockResponse = { dblist: [], userlist: [] }; + brokerService.updateDbmtUser.mockResolvedValue(mockResponse); + + const result = await controller.updateDbmtUser(req, 'host-uid-1', body); + + expect(brokerService.updateDbmtUser).toHaveBeenCalledWith( + 'user-123', + 'host-uid-1', + body + ); + expect(result).toEqual(mockResponse); + }); + }); }); diff --git a/apps/api-server/src/broker/broker.controller.ts b/apps/api-server/src/broker/broker.controller.ts index 2895e5d..221716e 100644 --- a/apps/api-server/src/broker/broker.controller.ts +++ b/apps/api-server/src/broker/broker.controller.ts @@ -1,5 +1,15 @@ -import { Controller, Get, Logger, Param, Post, Request } from '@nestjs/common'; -import { BrokerListClientResponse, GetBrokerStatusClientResponse } from '@api-interfaces'; +import { Body, Controller, Get, Logger, Param, Patch, Post, Request } from '@nestjs/common'; +import { + AddDbmtUserClientResponse, + AddDbmtUserRequest, + BrokerListClientResponse, + GetBrokerStatusClientResponse, + StartAllBrokersClientResponse, + StopAllBrokersClientResponse, + UpdateDbmtUserClientResponse, + UpdateDbmtUserRequest, +} from '@api-interfaces'; +import { validateRequiredFields } from '@util'; import { BaseCmsResponse } from '@type'; import { BrokerService } from './broker.service'; @@ -23,6 +33,105 @@ export class BrokerController { constructor(private readonly brokerService: BrokerService) {} + /** + * Start all brokers on a host (CMS task: startbroker). + * + * @route POST /:hostUid/broker/start-all + * @param req - Request object containing user information + * @param hostUid - Host unique identifier from path parameter + * @returns StartAllBrokersClientResponse { success: true } on success + * @example + * // POST /host-uid/broker/start-all + */ + @Post('start-all') + async startAllBrokers( + @Request() req, + @Param('hostUid') hostUid: string + ): Promise { + const userId = req.user.sub; + this.logger.log(`Starting all brokers on host: ${hostUid}`); + return await this.brokerService.startAllBrokers(userId, hostUid); + } + + /** + * Stop all brokers on a host (CMS task: stopbroker). + * + * @route POST /:hostUid/broker/stop-all + * @param req - Request object containing user information + * @param hostUid - Host unique identifier from path parameter + * @returns StopAllBrokersClientResponse { success: true } on success + * @example + * // POST /host-uid/broker/stop-all + */ + @Post('stop-all') + async stopAllBrokers( + @Request() req, + @Param('hostUid') hostUid: string + ): Promise { + const userId = req.user.sub; + this.logger.log(`Stopping all brokers on host: ${hostUid}`); + return await this.brokerService.stopAllBrokers(userId, hostUid); + } + + /** + * Add a DBMT (CMS) user on the host (CMS task: adddbmtuser). + * + * @route POST /:hostUid/broker/dbmt-user + * @param req - Request object containing user information + * @param hostUid - Host unique identifier from path parameter + * @param body - targetid, password, casauth, dbcreate, statusmonitorauth + * @returns AddDbmtUserClientResponse dblist and userlist + * @example + * // POST /host-uid/broker/dbmt-user + * // Body: { "targetid": "test_user_2", "password": "1234", "casauth": "none", "dbcreate": "none", "statusmonitorauth": "none" } + */ + @Post('dbmt-user') + async addDbmtUser( + @Request() req, + @Param('hostUid') hostUid: string, + @Body() body: AddDbmtUserRequest + ): Promise { + const userId = req.user.sub; + validateRequiredFields( + body, + ['targetid', 'password', 'casauth', 'dbcreate', 'statusmonitorauth'], + 'broker/dbmt-user', + this.logger + ); + this.logger.log(`Adding DBMT user: ${body.targetid} on host: ${hostUid}`); + return await this.brokerService.addDbmtUser(userId, hostUid, body); + } + + /** + * Update a DBMT (CMS) user on the host (CMS task: updatedbmtuser). + * + * @route PATCH /:hostUid/broker/dbmt-user + * @param req - Request object containing user information + * @param hostUid - Host unique identifier from path parameter + * @param body - targetid, casauth, dbcreate, statusmonitorauth (dbauth optional) + * @returns UpdateDbmtUserClientResponse dblist and userlist + * @example + * // PATCH /host-uid/broker/dbmt-user + * // Body: { "targetid": "test_user_2", "casauth": "none", "dbcreate": "none", "statusmonitorauth": "none" } + * // or with dbauth: { "targetid": "test_user_2", "dbauth": [], "casauth": "none", "dbcreate": "none", "statusmonitorauth": "none" } + */ + @Patch('dbmt-user') + async updateDbmtUser( + @Request() req, + @Param('hostUid') hostUid: string, + @Body() body: UpdateDbmtUserRequest + ): Promise { + const userId = req.user.sub; + validateRequiredFields( + body, + ['targetid', 'casauth', 'dbcreate', 'statusmonitorauth'], + 'broker/dbmt-user', + this.logger + ); + this.logger.log(`Updating DBMT user: ${body.targetid} on host: ${hostUid}`); + return await this.brokerService.updateDbmtUser(userId, hostUid, body); + } + /** * Get list of brokers for a specific host. * @@ -31,7 +140,7 @@ export class BrokerController { * @param hostUid - Host unique identifier from path parameter * @returns List of brokers * @example - * // POST /host-uid/broker/list + * // GET /host-uid/broker/list */ @Get('list') async getBrokers( @@ -63,7 +172,7 @@ export class BrokerController { ): Promise { const userId = req.user.sub; - Logger.log(`Stopping broker: ${bname} on host: ${hostUid}`, 'BrokerController'); + this.logger.log(`Stopping broker: ${bname} on host: ${hostUid}`); const response = await this.brokerService.stopBroker(userId, hostUid, bname); return response; } @@ -87,7 +196,7 @@ export class BrokerController { ): Promise { const userId = req.user.sub; - Logger.log(`Starting broker: ${bname} on host: ${hostUid}`, 'BrokerController'); + this.logger.log(`Starting broker: ${bname} on host: ${hostUid}`); const response = await this.brokerService.startBroker(userId, hostUid, bname); return response; } @@ -111,7 +220,7 @@ export class BrokerController { ): Promise { const userId = req.user.sub; - Logger.log(`Restarting broker: ${bname} on host: ${hostUid}`, 'BrokerController'); + this.logger.log(`Restarting broker: ${bname} on host: ${hostUid}`); const response: boolean = await this.brokerService.restartBroker(userId, hostUid, bname); return response; } @@ -135,7 +244,7 @@ export class BrokerController { ): Promise { const userId = req.user.sub; - Logger.log(`Getting broker status: ${bname} on host: ${hostUid}`, 'BrokerController'); + this.logger.log(`Getting broker status: ${bname} on host: ${hostUid}`); const response = await this.brokerService.getBrokerStatus(userId, hostUid, bname); return response; } diff --git a/apps/api-server/src/broker/broker.service.spec.ts b/apps/api-server/src/broker/broker.service.spec.ts index e519c5e..232a070 100644 --- a/apps/api-server/src/broker/broker.service.spec.ts +++ b/apps/api-server/src/broker/broker.service.spec.ts @@ -1,18 +1,242 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BrokerService } from './broker.service'; +import { HostService } from '@host'; +import { CmsHttpsClientService } from '@cms-https-client/cms-https-client.service'; +import { BrokerError } from '@error/broker/broker-error'; +import * as common from '@common'; + +jest.mock('@common', () => ({ + ...jest.requireActual('@common'), + checkCmsTokenError: jest.fn(), + checkCmsStatusError: jest.fn(), +})); describe('BrokerService', () => { let service: BrokerService; + let hostService: jest.Mocked; + let cmsClient: jest.Mocked; + + const mockHost = { + uid: 'host-uid-1', + id: 'host-1', + address: 'localhost', + port: 8001, + password: 'host-password', + token: 'test-token', + dbProfiles: {}, + }; + + const mockUserId = 'user-123'; + const mockHostUid = 'host-uid-1'; beforeEach(async () => { + const mockHostService = { findHostInternal: jest.fn() }; + const mockCmsClient = { postAuthenticated: jest.fn() }; + const module: TestingModule = await Test.createTestingModule({ - providers: [BrokerService], + providers: [ + BrokerService, + { provide: HostService, useValue: mockHostService }, + { provide: CmsHttpsClientService, useValue: mockCmsClient }, + ], }).compile(); - service = module.get(BrokerService); + service = module.get(BrokerService); + hostService = module.get(HostService); + cmsClient = module.get(CmsHttpsClientService); + + hostService.findHostInternal.mockResolvedValue(mockHost); + (common.checkCmsTokenError as jest.Mock).mockImplementation(() => {}); + (common.checkCmsStatusError as jest.Mock).mockImplementation(() => {}); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('startAllBrokers', () => { + it('should send startbroker task and return { success: true }', async () => { + const mockResponse = { + __EXEC_TIME: '72 ms', + note: 'none', + status: 'success', + task: 'startbroker', + }; + cmsClient.postAuthenticated.mockResolvedValue(mockResponse); + + const result = await service.startAllBrokers(mockUserId, mockHostUid); + + expect(hostService.findHostInternal).toHaveBeenCalledWith( + mockUserId, + mockHostUid + ); + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + `https://${mockHost.address}:${mockHost.port}/cm_api`, + expect.objectContaining({ + task: 'startbroker', + token: mockHost.token, + }) + ); + expect(result).toEqual({ success: true }); + }); + + it('should throw BrokerError when CMS status is not success', async () => { + cmsClient.postAuthenticated.mockResolvedValue({ + __EXEC_TIME: '0 ms', + note: 'failed', + status: 'fail', + task: 'startbroker', + }); + + await expect( + service.startAllBrokers(mockUserId, mockHostUid) + ).rejects.toThrow(BrokerError); + }); + }); + + describe('stopAllBrokers', () => { + it('should send stopbroker task and return { success: true }', async () => { + const mockResponse = { + __EXEC_TIME: '72 ms', + note: 'none', + status: 'success', + task: 'stopbroker', + }; + cmsClient.postAuthenticated.mockResolvedValue(mockResponse); + + const result = await service.stopAllBrokers(mockUserId, mockHostUid); + + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + task: 'stopbroker', + token: mockHost.token, + }) + ); + expect(result).toEqual({ success: true }); + }); + + it('should throw BrokerError when CMS status is not success', async () => { + cmsClient.postAuthenticated.mockResolvedValue({ + __EXEC_TIME: '0 ms', + note: 'failed', + status: 'fail', + task: 'stopbroker', + }); + + await expect( + service.stopAllBrokers(mockUserId, mockHostUid) + ).rejects.toThrow(BrokerError); + }); + }); + + describe('addDbmtUser', () => { + const mockRequest = { + targetid: 'test_user_2', + password: '1234', + casauth: 'none', + dbcreate: 'none', + statusmonitorauth: 'none', + }; + + it('should send adddbmtuser task and return dblist and userlist', async () => { + const mockResponse = { + __EXEC_TIME: '1 ms', + note: 'none', + status: 'success', + task: 'adddbmtuser', + dblist: [{ dbs: [{ dbname: 'test' }] }], + userlist: [{ user: [{ '@id': 'test_user_2' }] }], + }; + cmsClient.postAuthenticated.mockResolvedValue(mockResponse); + + const result = await service.addDbmtUser(mockUserId, mockHostUid, mockRequest); + + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + task: 'adddbmtuser', + targetid: mockRequest.targetid, + password: mockRequest.password, + casauth: mockRequest.casauth, + dbcreate: mockRequest.dbcreate, + statusmonitorauth: mockRequest.statusmonitorauth, + }) + ); + expect(result).toEqual({ + dblist: mockResponse.dblist, + userlist: mockResponse.userlist, + }); + }); + + it('should throw BrokerError when CMS status is not success', async () => { + cmsClient.postAuthenticated.mockResolvedValue({ + __EXEC_TIME: '0 ms', + note: 'failed', + status: 'fail', + task: 'adddbmtuser', + }); + + await expect( + service.addDbmtUser(mockUserId, mockHostUid, mockRequest) + ).rejects.toThrow(BrokerError); + }); + }); + + describe('updateDbmtUser', () => { + const mockRequest = { + targetid: 'test_user_2', + dbauth: [] as unknown[], + casauth: 'none', + dbcreate: 'none', + statusmonitorauth: 'none', + }; + + it('should send updatedbmtuser task and return dblist and userlist', async () => { + const mockResponse = { + __EXEC_TIME: '0 ms', + note: 'none', + status: 'success', + task: 'updatedbmtuser', + dblist: [{ dbs: [{ dbname: 'test' }] }], + userlist: [{ user: [{ '@id': 'test_user_2' }] }], + }; + cmsClient.postAuthenticated.mockResolvedValue(mockResponse); + + const result = await service.updateDbmtUser(mockUserId, mockHostUid, mockRequest); + + expect(cmsClient.postAuthenticated).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + task: 'updatedbmtuser', + targetid: mockRequest.targetid, + dbauth: mockRequest.dbauth, + casauth: mockRequest.casauth, + dbcreate: mockRequest.dbcreate, + statusmonitorauth: mockRequest.statusmonitorauth, + }) + ); + expect(result).toEqual({ + dblist: mockResponse.dblist, + userlist: mockResponse.userlist, + }); + }); + + it('should throw BrokerError when CMS status is not success', async () => { + cmsClient.postAuthenticated.mockResolvedValue({ + __EXEC_TIME: '0 ms', + note: 'failed', + status: 'fail', + task: 'updatedbmtuser', + }); + + await expect( + service.updateDbmtUser(mockUserId, mockHostUid, mockRequest) + ).rejects.toThrow(BrokerError); + }); + }); }); diff --git a/apps/api-server/src/broker/broker.service.ts b/apps/api-server/src/broker/broker.service.ts index d64bb5a..e127ac7 100644 --- a/apps/api-server/src/broker/broker.service.ts +++ b/apps/api-server/src/broker/broker.service.ts @@ -3,15 +3,33 @@ import { BaseService, HandleBrokerErrors } from '@common'; import { BrokerError } from '@error/broker/broker-error'; import { HostService } from '@host'; import { Injectable } from '@nestjs/common'; -import { GetBrokerStatusClientResponse } from '@api-interfaces'; import { + AddDbmtUserClientResponse, + AddDbmtUserRequest, + GetBrokerStatusClientResponse, + StartAllBrokersClientResponse, + StopAllBrokersClientResponse, + UpdateDbmtUserClientResponse, + UpdateDbmtUserRequest, +} from '@api-interfaces'; +import { + AddDbmtUserCmsRequest, BaseCmsRequest, BaseCmsResponse, GetBrokerStatusCmsRequest, GetBrokerStatusCmsResponse, GetBrokersInfoCmsResponse, HandleBrokerCmsRequest, + StartBrokerCmsRequest, + StopBrokerCmsRequest, + UpdateDbmtUserCmsRequest, } from '@type'; +import { + AddDbmtUserCmsResponse, + StartBrokerCmsResponse, + StopBrokerCmsResponse, + UpdateDbmtUserCmsResponse, +} from '@type/cms-response'; /** * Service for managing broker operations. @@ -31,6 +49,84 @@ export class BrokerService extends BaseService { super(hostService, cmsClient); } + /** + * Add a DBMT (CMS) user on the host. + * CMS task: adddbmtuser. + * + * @param userId User ID from JWT + * @param hostUid Host UID + * @param request targetid, password, casauth, dbcreate, statusmonitorauth + * @returns AddDbmtUserClientResponse dblist and userlist (domain data only) + */ + @HandleBrokerErrors() + async addDbmtUser( + userId: string, + hostUid: string, + request: AddDbmtUserRequest + ): Promise { + const cmsRequest: AddDbmtUserCmsRequest = { + task: 'adddbmtuser', + targetid: request.targetid, + password: request.password, + casauth: request.casauth, + dbcreate: request.dbcreate, + statusmonitorauth: request.statusmonitorauth, + }; + + const response = await this.executeCmsRequest< + AddDbmtUserCmsRequest, + AddDbmtUserCmsResponse + >(userId, hostUid, cmsRequest); + + if (response.status !== 'success') { + throw BrokerError.AddDbmtUserFailed({ response }); + } + + return { + dblist: response.dblist ?? [], + userlist: response.userlist ?? [], + }; + } + + /** + * Update a DBMT (CMS) user on the host. + * CMS task: updatedbmtuser. + * + * @param userId User ID from JWT + * @param hostUid Host UID + * @param request targetid, dbauth, casauth, dbcreate, statusmonitorauth (no password) + * @returns UpdateDbmtUserClientResponse dblist and userlist (domain data only) + */ + @HandleBrokerErrors() + async updateDbmtUser( + userId: string, + hostUid: string, + request: UpdateDbmtUserRequest + ): Promise { + const cmsRequest: UpdateDbmtUserCmsRequest = { + task: 'updatedbmtuser', + targetid: request.targetid, + dbauth: request.dbauth ?? [], + casauth: request.casauth, + dbcreate: request.dbcreate, + statusmonitorauth: request.statusmonitorauth, + }; + + const response = await this.executeCmsRequest< + UpdateDbmtUserCmsRequest, + UpdateDbmtUserCmsResponse + >(userId, hostUid, cmsRequest); + + if (response.status !== 'success') { + throw BrokerError.UpdateDbmtUserFailed({ response }); + } + + return { + dblist: response.dblist ?? [], + userlist: response.userlist ?? [], + }; + } + @HandleBrokerErrors() async getBrokers(userId: string, hostUid: string) { const cmsRequest: BaseCmsRequest = { @@ -151,36 +247,40 @@ export class BrokerService extends BaseService { } @HandleBrokerErrors() - async stopAllBrokers(userId: string, hostUid: string): Promise { - const cmsRequest: BaseCmsRequest = { + async stopAllBrokers( + userId: string, + hostUid: string + ): Promise { + const cmsRequest: StopBrokerCmsRequest = { task: 'stopbroker', }; - const response = await this.executeCmsRequest( - userId, - hostUid, - cmsRequest - ); + const response = await this.executeCmsRequest< + StopBrokerCmsRequest, + StopBrokerCmsResponse + >(userId, hostUid, cmsRequest); if (response.status === 'success') { - return true; + return { success: true }; } throw BrokerError.BrokerStopFailed(); } @HandleBrokerErrors() - async startAllBrokers(userId: string, hostUid: string): Promise { - const cmsRequest: BaseCmsRequest = { + async startAllBrokers( + userId: string, + hostUid: string + ): Promise { + const cmsRequest: StartBrokerCmsRequest = { task: 'startbroker', }; - const response = await this.executeCmsRequest( - userId, - hostUid, - cmsRequest - ); + const response = await this.executeCmsRequest< + StartBrokerCmsRequest, + StartBrokerCmsResponse + >(userId, hostUid, cmsRequest); if (response.status === 'success') { - return true; + return { success: true }; } throw BrokerError.BrokerStartFailed(); diff --git a/apps/api-server/src/error/broker/broker-error-code.ts b/apps/api-server/src/error/broker/broker-error-code.ts index 05c8d66..66eaaf1 100644 --- a/apps/api-server/src/error/broker/broker-error-code.ts +++ b/apps/api-server/src/error/broker/broker-error-code.ts @@ -8,6 +8,8 @@ export enum BrokerErrorCode { GET_BROKER_FAILED = 'GET_BROKER_FAILED', BROKER_STOP_FAILED = 'BROKER_STOP_FAILED', BROKER_START_FAILED = 'BROKER_START_FAILED', + ADD_DBMT_USER_FAILED = 'ADD_DBMT_USER_FAILED', + UPDATE_DBMT_USER_FAILED = 'UPDATE_DBMT_USER_FAILED', INTERNAL_ERROR = 'INTERNAL_ERROR', UNKNOWN = 'UNKNOWN', } diff --git a/apps/api-server/src/error/broker/broker-error.ts b/apps/api-server/src/error/broker/broker-error.ts index aa273e1..a119941 100644 --- a/apps/api-server/src/error/broker/broker-error.ts +++ b/apps/api-server/src/error/broker/broker-error.ts @@ -48,6 +48,30 @@ export class BrokerError extends AppError { ); } + /** + * Creates an error indicating that adding DBMT user failed. + */ + static AddDbmtUserFailed(additionalData?: Record, originalError?: Error) { + return new BrokerError( + 'BROKER', + BrokerErrorCode.ADD_DBMT_USER_FAILED, + additionalData, + originalError + ); + } + + /** + * Creates an error indicating that updating DBMT user failed. + */ + static UpdateDbmtUserFailed(additionalData?: Record, originalError?: Error) { + return new BrokerError( + 'BROKER', + BrokerErrorCode.UPDATE_DBMT_USER_FAILED, + additionalData, + originalError + ); + } + /** * Creates an error for an unknown broker-related issue. */ diff --git a/apps/api-server/src/type/cms-request/add-dbmt-user-cms-request.ts b/apps/api-server/src/type/cms-request/add-dbmt-user-cms-request.ts new file mode 100644 index 0000000..c6452ff --- /dev/null +++ b/apps/api-server/src/type/cms-request/add-dbmt-user-cms-request.ts @@ -0,0 +1,19 @@ +import { BaseCmsRequest } from './base-cms-request'; + +/** + * CMS request for adding a DBMT (CMS) user. + * Task: adddbmtuser. + */ +export type AddDbmtUserCmsRequest = BaseCmsRequest & { + task: 'adddbmtuser'; + /** Target user id (login name) */ + targetid: string; + /** Password */ + password: string; + /** CAS auth: 'none' | 'admin' | 'monitor' */ + casauth: string; + /** DB create auth: 'none' | 'admin' */ + dbcreate: string; + /** Status monitor auth: 'none' | 'admin' */ + statusmonitorauth: string; +}; diff --git a/apps/api-server/src/type/cms-request/handle-broker-cms-request.ts b/apps/api-server/src/type/cms-request/handle-broker-cms-request.ts new file mode 100644 index 0000000..e354099 --- /dev/null +++ b/apps/api-server/src/type/cms-request/handle-broker-cms-request.ts @@ -0,0 +1,10 @@ +import { BaseCmsRequest } from './base-cms-request'; + +/** + * CMS request for starting or stopping a single broker by name. + * Task: 'broker_start' | 'broker_stop', bname required. + */ +export type HandleBrokerCmsRequest = BaseCmsRequest & { + task: 'broker_start' | 'broker_stop'; + bname: string; +}; diff --git a/apps/api-server/src/type/cms-request/index.ts b/apps/api-server/src/type/cms-request/index.ts index 142c631..c28fd41 100644 --- a/apps/api-server/src/type/cms-request/index.ts +++ b/apps/api-server/src/type/cms-request/index.ts @@ -5,6 +5,8 @@ export * from './login-cms-request'; export * from './login-db-cms-request'; export * from './base-cms-forward-request'; export * from './cms-forward-request-without-token'; +export * from './handle-broker-cms-request'; +export * from './start-broker-cms-request'; export * from './stop-broker-cms-request'; export * from './get-broker-status-cms-request'; export * from './get-env-cms-request'; @@ -38,6 +40,8 @@ export * from './check-database-cms-request'; export * from './compact-database-cms-request'; export * from './rename-database-cms-request'; export * from './get-add-vol-status-cms-request'; +export * from './add-dbmt-user-cms-request'; +export * from './update-dbmt-user-cms-request'; export * from './add-vol-db-cms-request'; export * from './lock-database-cms-request'; export * from './get-auto-exec-query-err-log-cms-request'; diff --git a/apps/api-server/src/type/cms-request/start-broker-cms-request.ts b/apps/api-server/src/type/cms-request/start-broker-cms-request.ts new file mode 100644 index 0000000..d25affd --- /dev/null +++ b/apps/api-server/src/type/cms-request/start-broker-cms-request.ts @@ -0,0 +1,9 @@ +import { BaseCmsRequest } from './base-cms-request'; + +/** + * CMS request for starting all brokers on a host. + * Task: startbroker (no additional parameters). + */ +export type StartBrokerCmsRequest = BaseCmsRequest & { + task: 'startbroker'; +}; diff --git a/apps/api-server/src/type/cms-request/stop-broker-cms-request.ts b/apps/api-server/src/type/cms-request/stop-broker-cms-request.ts index e19845d..edd3f82 100644 --- a/apps/api-server/src/type/cms-request/stop-broker-cms-request.ts +++ b/apps/api-server/src/type/cms-request/stop-broker-cms-request.ts @@ -1,3 +1,9 @@ import { BaseCmsRequest } from './base-cms-request'; -export type HandleBrokerCmsRequest = BaseCmsRequest & { bname: string }; +/** + * CMS request for stopping all brokers on a host. + * Task: stopbroker (no additional parameters). + */ +export type StopBrokerCmsRequest = BaseCmsRequest & { + task: 'stopbroker'; +}; diff --git a/apps/api-server/src/type/cms-request/update-dbmt-user-cms-request.ts b/apps/api-server/src/type/cms-request/update-dbmt-user-cms-request.ts new file mode 100644 index 0000000..1bbf81b --- /dev/null +++ b/apps/api-server/src/type/cms-request/update-dbmt-user-cms-request.ts @@ -0,0 +1,20 @@ +import { BaseCmsRequest } from './base-cms-request'; + +/** + * CMS request for updating a DBMT (CMS) user. + * Task: updatedbmtuser. + * No password in request; updates auth settings only. + */ +export type UpdateDbmtUserCmsRequest = BaseCmsRequest & { + task: 'updatedbmtuser'; + /** Target user id (login name) */ + targetid: string; + /** DB auth array (e.g. [] for none). Optional; omitted or undefined is treated as []. */ + dbauth?: unknown[]; + /** CAS auth: 'none' | 'admin' | 'monitor' */ + casauth: string; + /** DB create auth: 'none' | 'admin' */ + dbcreate: string; + /** Status monitor auth: 'none' | 'admin' */ + statusmonitorauth: string; +}; diff --git a/apps/api-server/src/type/cms-response/add-dbmt-user-cms-response.ts b/apps/api-server/src/type/cms-response/add-dbmt-user-cms-response.ts new file mode 100644 index 0000000..2ac12e0 --- /dev/null +++ b/apps/api-server/src/type/cms-response/add-dbmt-user-cms-response.ts @@ -0,0 +1,20 @@ +import { BaseCmsResponse } from './base-cms-response'; + +/** DB list item in adddbmtuser response */ +export type AddDbmtUserDblistItem = { + dbs: Array<{ dbname: string }>; +}; + +/** User list item in adddbmtuser response (per broker) */ +export type AddDbmtUserUserlistItem = { + user: Array>; +}; + +/** + * CMS response for adddbmtuser task. + */ +export type AddDbmtUserCmsResponse = BaseCmsResponse & { + task: 'adddbmtuser'; + dblist?: AddDbmtUserDblistItem[]; + userlist?: AddDbmtUserUserlistItem[]; +}; diff --git a/apps/api-server/src/type/cms-response/index.ts b/apps/api-server/src/type/cms-response/index.ts index 6952ad3..ca34089 100644 --- a/apps/api-server/src/type/cms-response/index.ts +++ b/apps/api-server/src/type/cms-response/index.ts @@ -4,6 +4,8 @@ export * from './check-file-cms-response'; export * from './base-cms-response'; export * from './get-brokers-info-cms-response'; export * from './get-broker-status-cms-response'; +export * from './start-broker-cms-response'; +export * from './stop-broker-cms-response'; export * from './get-env-cms-response'; export * from './start-info-cms-response'; export * from './paramdump-cms-response'; @@ -41,3 +43,5 @@ export * from './get-auto-backup-db-err-log-cms-response'; export * from './get-transaction-info-cms-response'; export * from './kill-transaction-cms-response'; export * from './delete-database-cms-response'; +export * from './add-dbmt-user-cms-response'; +export * from './update-dbmt-user-cms-response'; diff --git a/apps/api-server/src/type/cms-response/start-broker-cms-response.ts b/apps/api-server/src/type/cms-response/start-broker-cms-response.ts new file mode 100644 index 0000000..15eabb0 --- /dev/null +++ b/apps/api-server/src/type/cms-response/start-broker-cms-response.ts @@ -0,0 +1,8 @@ +import { BaseCmsResponse } from './base-cms-response'; + +/** + * CMS response for startbroker task. + */ +export type StartBrokerCmsResponse = BaseCmsResponse & { + task: 'startbroker'; +}; diff --git a/apps/api-server/src/type/cms-response/stop-broker-cms-response.ts b/apps/api-server/src/type/cms-response/stop-broker-cms-response.ts new file mode 100644 index 0000000..1c30faa --- /dev/null +++ b/apps/api-server/src/type/cms-response/stop-broker-cms-response.ts @@ -0,0 +1,8 @@ +import { BaseCmsResponse } from './base-cms-response'; + +/** + * CMS response for stopbroker task. + */ +export type StopBrokerCmsResponse = BaseCmsResponse & { + task: 'stopbroker'; +}; diff --git a/apps/api-server/src/type/cms-response/update-dbmt-user-cms-response.ts b/apps/api-server/src/type/cms-response/update-dbmt-user-cms-response.ts new file mode 100644 index 0000000..62c166e --- /dev/null +++ b/apps/api-server/src/type/cms-response/update-dbmt-user-cms-response.ts @@ -0,0 +1,20 @@ +import { BaseCmsResponse } from './base-cms-response'; + +/** DB list item in updatedbmtuser response */ +export type UpdateDbmtUserDblistItem = { + dbs: Array<{ dbname: string }>; +}; + +/** User list item in updatedbmtuser response (per broker) */ +export type UpdateDbmtUserUserlistItem = { + user: Array>; +}; + +/** + * CMS response for updatedbmtuser task. + */ +export type UpdateDbmtUserCmsResponse = BaseCmsResponse & { + task: 'updatedbmtuser'; + dblist?: UpdateDbmtUserDblistItem[]; + userlist?: UpdateDbmtUserUserlistItem[]; +}; diff --git a/libs/api-interfaces/src/request/add-dbmt-user-request.ts b/libs/api-interfaces/src/request/add-dbmt-user-request.ts new file mode 100644 index 0000000..917ca2d --- /dev/null +++ b/libs/api-interfaces/src/request/add-dbmt-user-request.ts @@ -0,0 +1,15 @@ +/** + * Client request for adding a DBMT (CMS) user. + */ +export type AddDbmtUserRequest = { + /** Target user id (login name) */ + targetid: string; + /** Password */ + password: string; + /** CAS auth: 'none' | 'admin' | 'monitor' */ + casauth: string; + /** DB create auth: 'none' | 'admin' */ + dbcreate: string; + /** Status monitor auth: 'none' | 'admin' */ + statusmonitorauth: string; +}; diff --git a/libs/api-interfaces/src/request/index.ts b/libs/api-interfaces/src/request/index.ts index cb978b1..503e91b 100644 --- a/libs/api-interfaces/src/request/index.ts +++ b/libs/api-interfaces/src/request/index.ts @@ -51,3 +51,5 @@ export * from './get-auto-backup-db-err-log-request'; export * from './get-transaction-info-request'; export * from './kill-transaction-request'; export * from './delete-database-request'; +export * from './add-dbmt-user-request'; +export * from './update-dbmt-user-request'; diff --git a/libs/api-interfaces/src/request/update-dbmt-user-request.ts b/libs/api-interfaces/src/request/update-dbmt-user-request.ts new file mode 100644 index 0000000..e58067c --- /dev/null +++ b/libs/api-interfaces/src/request/update-dbmt-user-request.ts @@ -0,0 +1,16 @@ +/** + * Client request for updating a DBMT (CMS) user. + * No password; updates auth settings only. + */ +export type UpdateDbmtUserRequest = { + /** Target user id (login name) */ + targetid: string; + /** DB auth array (e.g. [] for none). Optional; omitted is treated as []. */ + dbauth?: unknown[]; + /** CAS auth: 'none' | 'admin' | 'monitor' */ + casauth: string; + /** DB create auth: 'none' | 'admin' */ + dbcreate: string; + /** Status monitor auth: 'none' | 'admin' */ + statusmonitorauth: string; +}; diff --git a/libs/api-interfaces/src/response/add-dbmt-user-response.ts b/libs/api-interfaces/src/response/add-dbmt-user-response.ts new file mode 100644 index 0000000..94ce6c8 --- /dev/null +++ b/libs/api-interfaces/src/response/add-dbmt-user-response.ts @@ -0,0 +1,17 @@ +/** DB list item in adddbmtuser response */ +export type AddDbmtUserDblistItem = { + dbs: Array<{ dbname: string }>; +}; + +/** User list item in adddbmtuser response (per broker) */ +export type AddDbmtUserUserlistItem = { + user: Array>; +}; + +/** + * Client response for adddbmtuser (domain data only). + */ +export type AddDbmtUserClientResponse = { + dblist: AddDbmtUserDblistItem[]; + userlist: AddDbmtUserUserlistItem[]; +}; diff --git a/libs/api-interfaces/src/response/index.ts b/libs/api-interfaces/src/response/index.ts index aae993d..f232d77 100644 --- a/libs/api-interfaces/src/response/index.ts +++ b/libs/api-interfaces/src/response/index.ts @@ -10,6 +10,8 @@ export * from './standard-response'; export * from './host-client-response'; export * from './broker-client-response'; export * from './broker-status-client-response'; +export * from './start-all-brokers-response'; +export * from './stop-all-brokers-response'; export * from './cms-file-client-response'; export * from './database-client-response'; export * from './database-volume-info-client-response'; @@ -53,3 +55,5 @@ export * from './get-auto-backup-db-err-log-response'; export * from './get-transaction-info-response'; export * from './kill-transaction-response'; export * from './delete-database-response'; +export * from './add-dbmt-user-response'; +export * from './update-dbmt-user-response'; diff --git a/libs/api-interfaces/src/response/start-all-brokers-response.ts b/libs/api-interfaces/src/response/start-all-brokers-response.ts new file mode 100644 index 0000000..9ab39ff --- /dev/null +++ b/libs/api-interfaces/src/response/start-all-brokers-response.ts @@ -0,0 +1,6 @@ +/** + * Client response for start all brokers (task: startbroker). + */ +export type StartAllBrokersClientResponse = { + success: boolean; +}; diff --git a/libs/api-interfaces/src/response/stop-all-brokers-response.ts b/libs/api-interfaces/src/response/stop-all-brokers-response.ts new file mode 100644 index 0000000..83fc4f8 --- /dev/null +++ b/libs/api-interfaces/src/response/stop-all-brokers-response.ts @@ -0,0 +1,6 @@ +/** + * Client response for stop all brokers (task: stopbroker). + */ +export type StopAllBrokersClientResponse = { + success: boolean; +}; diff --git a/libs/api-interfaces/src/response/update-dbmt-user-response.ts b/libs/api-interfaces/src/response/update-dbmt-user-response.ts new file mode 100644 index 0000000..5526837 --- /dev/null +++ b/libs/api-interfaces/src/response/update-dbmt-user-response.ts @@ -0,0 +1,17 @@ +/** DB list item in updatedbmtuser response */ +export type UpdateDbmtUserDblistItem = { + dbs: Array<{ dbname: string }>; +}; + +/** User list item in updatedbmtuser response (per broker) */ +export type UpdateDbmtUserUserlistItem = { + user: Array>; +}; + +/** + * Client response for updatedbmtuser (domain data only). + */ +export type UpdateDbmtUserClientResponse = { + dblist: UpdateDbmtUserDblistItem[]; + userlist: UpdateDbmtUserUserlistItem[]; +};