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/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, 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 f163da2..79069e0 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,11 @@ -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, GetCreatedbInfoClientResponse, StartInfoClientResponse, SaveDatabaseProfileRequest, @@ -256,4 +257,34 @@ 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. + * 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 StartInfoClientResponse Latest database list (start-info) 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..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,11 +5,14 @@ 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'; 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 @@ -28,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', @@ -65,11 +69,13 @@ describe('DatabaseLifecycleService', () => { const mockDatabaseConfigService = { setAutoAddVol: jest.fn(), setAutoStart: jest.fn(), + removeAutoStart: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ DatabaseLifecycleService, + DatabaseInfoService, { provide: HostService, useValue: mockHostService, @@ -109,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); @@ -220,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 () => { @@ -268,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 () => { @@ -322,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 () => { @@ -372,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 () => { @@ -508,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({}); @@ -775,4 +791,123 @@ 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', + }; + + 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', + }; + + 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(mockStartInfoAfterDelete); + }); + + 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', + }; + + 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(mockStartInfoAfterDelete); + }); + + 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..d54a698 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,7 @@ import { CreateDatabaseWithConfigRequest, CreateDatabaseWithConfigResponse, DatabaseVolumeInfoClientResponse, + DeleteDatabaseRequest, StartInfoClientResponse, } from '@api-interfaces'; import { GetCreatedbInfoClientResponse } from '@api-interfaces/response/get-createdb-info-client-response'; @@ -11,8 +12,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'; @@ -20,20 +24,22 @@ 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'; import { CreateDatabaseCmsRequest, + DeleteDatabaseCmsRequest, DbSpaceInfoCmsRequest, StartDatabaseCmsRequest, StopDatabaseCmsRequest, } from '@type/cms-request'; import { CreateDatabaseCmsResponse, + DeleteDatabaseCmsResponse, DbSpaceInfoCmsResponse, - StartInfoCmsResponse, } from '@type/cms-response'; import { convertExvolArrayToCmsFormat } from '@util'; @@ -53,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); } /** @@ -143,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 }); @@ -176,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 }); @@ -220,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, @@ -290,7 +249,7 @@ export class DatabaseLifecycleService extends BaseService { return user; }); - return await this.startInfo(userId, hostUid); + return await this.databaseInfoService.startInfo(userId, hostUid); } /** @@ -309,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)); @@ -344,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. @@ -608,4 +540,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 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 StartInfoClientResponse Latest database list (start-info) 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, + } + ); + } + } + + // 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 bec38ef..6bf9c8d 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, @@ -20,7 +18,7 @@ import { OptimizeDatabaseRequest, OptimizeDatabaseResponse, RenameDatabaseRequest, - RenameDatabaseResponse, + StartInfoClientResponse, UnloadDatabaseRequest, UnloadInfoClientResponse, } from '@api-interfaces'; @@ -241,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" } @@ -252,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( @@ -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..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'; @@ -14,7 +15,6 @@ import { LockDatabaseRequest, GetTransactionInfoRequest, KillTransactionRequest, - DeleteDatabaseRequest, } from '@api-interfaces'; import { UnloadDatabaseCmsResponse, @@ -25,7 +25,6 @@ import { LockDatabaseCmsResponse, GetTransactionInfoCmsResponse, KillTransactionCmsResponse, - DeleteDatabaseCmsResponse, } from '@type/cms-response'; import * as common from '@common'; @@ -64,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, @@ -75,6 +79,10 @@ describe('DatabaseManagementService', () => { provide: CmsHttpsClientService, useValue: mockCmsClient, }, + { + provide: DatabaseInfoService, + useValue: mockDatabaseInfoService, + }, ], }).compile(); @@ -939,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 () => { @@ -971,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 () => { @@ -994,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 () => { @@ -1852,115 +1860,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..07a0292 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, @@ -19,32 +17,28 @@ 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, 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'; import { Injectable } from '@nestjs/common'; import { BaseService } from '@common'; -import { DATABASE_CONSTANTS } from '../database.constants'; import { AddVolDbCmsRequest, CheckDatabaseCmsRequest, CompactDatabaseCmsRequest, - DeleteDatabaseCmsRequest, GetAddVolStatusCmsRequest, LoadDatabaseCmsRequest, LockDatabaseCmsRequest, @@ -59,7 +53,6 @@ import { AddVolDbCmsResponse, CheckDatabaseCmsResponse, CompactDatabaseCmsResponse, - DeleteDatabaseCmsResponse, GetAddVolStatusCmsResponse, LoadDatabaseCmsResponse, LockDatabaseCmsResponse, @@ -83,7 +76,7 @@ export class DatabaseManagementService extends BaseService { constructor( hostService: HostService, cmsClient: CmsHttpsClientService, - private readonly databaseConfigService: DatabaseConfigService + private readonly databaseInfoService: DatabaseInfoService ) { super(hostService, cmsClient); } @@ -374,13 +367,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() @@ -390,7 +383,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', @@ -412,13 +405,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); } /** @@ -629,74 +622,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 {}; - } } 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[]; +};