using CMSDAL.CustomCode.DataSource; using CMSDAL.DTO.DataSource; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.EventLogPublish; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Newtonsoft.Json; using System.ComponentModel.DataAnnotations; using static GB5Shared.GB5Constant.Constant; namespace CMSBLL.DataSource; public class DataSourceBLL : IDataSourceBLL { private readonly IDataSourceDAL _dal; private readonly BaseEntityAppService _entityApp; private readonly EventLogPublish _eventLogPublish; public DataSourceBLL(IDataSourceDAL dal, BaseEntityAppService entityApp, EventLogPublish eventLogPublish) { _dal = dal; _entityApp = entityApp; _eventLogPublish = eventLogPublish; } public async Task GetDataSourceList(LoginDTO loginDTO, CancellationToken ct = default) { try { return await _dal.GetListAsync(loginDTO, ct); } catch { throw; } } public async Task GetDataSource(int dataSourceId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _dal.GetByIdAsync(dataSourceId, loginDTO, ct); } catch { throw; } } public async Task GetByCodeAsync(string dataSourceCode, LoginDTO loginDTO, CancellationToken ct = default) { try { var json = await _dal.GetByCodeAsync(dataSourceCode, loginDTO, ct); return JsonConvert.DeserializeObject(json); } catch { throw; } } public async Task GetByIdTypedAsync(int dataSourceId, LoginDTO loginDTO, CancellationToken ct = default) { try { var json = await _dal.GetByIdAsync(dataSourceId, loginDTO, ct); return JsonConvert.DeserializeObject(json); } catch { throw; } } public async Task SaveDataSource(DataSourceDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { var isNew = dto.DataSourceId == 0; var eventTypeId = isNew ? EventTypeConstant.SAVECMSDATASOURCEEVENTTYPEID : EventTypeConstant.UPDATECMSDATASOURCEEVENTTYPEID; try { GB5Trace.Step("validate-datasource", new { dto.DataSourceId, isNew }); if (string.IsNullOrWhiteSpace(dto.DataSourceCode)) throw new ValidationException("DataSourceCode is required."); if (string.IsNullOrWhiteSpace(dto.EndpointTemplate)) throw new ValidationException("EndpointTemplate is required."); if (dto.AuthMode != DataSourceAuthMode.None && string.IsNullOrWhiteSpace(dto.VaultSecretPath)) throw new ValidationException("VaultSecretPath is required when AuthMode is not None — secrets are never stored inline."); // A DataSource is a reusable definition (like MBLOCK/MCONTENTCONTAINERTYPE): // TenantId=-1 (system-wide, shared across every tenant) is a legitimate value to // have, but only as a migration seed — never something this save path hands to a // caller on request. dto.TenantId is never trusted directly here (a hidden form // field is still client-controlled input); a brand-new row always gets the caller's // own tenant, exactly like DataSourceDAL.SaveAsync/UpdateAsync's own explicit // TenantId = loginDTO.ClientId binding — this is belt-and-suspenders with that, not // a substitute for it. if (isNew) dto.TenantId = loginDTO.ClientId; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; if (isNew) { dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-datasource", new { dto.DataSourceId, isNew }); await _entityApp.ExecuteSaveAsync( EntityConstant.OBJECTCMSDATASOURCE, eventTypeId, dto, loginDTO, persistFunc: async tx => isNew ? await _dal.SaveAsync(dto, loginDTO, tx, ct).ConfigureAwait(false) : await _dal.UpdateAsync(dto, loginDTO, tx, ct).ConfigureAwait(false), isNewEntity: isNew); GB5Trace.Step("event-publish", new { EventTypeId = eventTypeId }); await _eventLogPublish.PublishEventLogAsync( isNew ? "CMS Data Source Created" : "CMS Data Source Updated", dto, eventTypeId, dto.DataSourceId, loginDTO, ct: ct); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.DataSourceId}" : SuccessResponse.UpdateSuccess; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-datasource-validation-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-datasource-failed", ex); throw; } } }