using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using AnalyticsBLL.DataSource; using AnalyticsBLL.Workspace; using AnalyticsDAL.CustomCode.BICatalog; using AnalyticsDAL.CustomCode.BIFieldMapping; using AnalyticsDAL.CustomCode.AnalysisWorkspace; using AnalyticsDAL.CustomCode.Warehouse; using AnalyticsDAL.DTO.BICatalog; using AnalyticsDAL.DTO.AnalysisWorkspace; using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace AnalyticsBLL.DatasetResolver { public class DatasetResolver : IDatasetResolver { private readonly IBICatalogDAL _catalogDAL; private readonly IWorkspaceBLL _workspaceBLL; private readonly IDataSourceBLL _dataSourceBLL; private readonly IWarehouseDAL _warehouseDAL; private readonly AnalyticsDAL.CustomCode.AnalysisQuery.IAnalysisQueryDAL _analysisQueryDAL; private readonly IAnalysisWorkspaceDAL _analysisWorkspaceDAL; private readonly IBIFieldMappingDAL _fieldMappingDAL; private readonly ILogger _logger; public DatasetResolver( IBICatalogDAL catalogDAL, IWorkspaceBLL workspaceBLL, IDataSourceBLL dataSourceBLL, IWarehouseDAL warehouseDAL, AnalyticsDAL.CustomCode.AnalysisQuery.IAnalysisQueryDAL analysisQueryDAL, IAnalysisWorkspaceDAL analysisWorkspaceDAL, IBIFieldMappingDAL fieldMappingDAL, ILogger logger) { _catalogDAL = catalogDAL; _workspaceBLL = workspaceBLL; _dataSourceBLL = dataSourceBLL; _warehouseDAL = warehouseDAL; _analysisQueryDAL = analysisQueryDAL; _analysisWorkspaceDAL = analysisWorkspaceDAL; _fieldMappingDAL = fieldMappingDAL; _logger = logger; } public async Task ResolveAsync(int biCatalogId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("resolve-dataset", new { biCatalogId }); var catalogEntry = await _catalogDAL.GetCatalogEntryById(biCatalogId, login, ct).ConfigureAwait(false); if (catalogEntry is null) throw new ArgumentException($"Dataset {biCatalogId} does not exist.", nameof(biCatalogId)); if (catalogEntry.TenantId != -1 && catalogEntry.TenantId != login.ClientId) throw new ArgumentException( $"Dataset {biCatalogId} is private to another tenant and is not resolvable here.", nameof(biCatalogId)); if (catalogEntry.WorkspaceId != -1) await _workspaceBLL.ValidateWorkspaceVisibility(catalogEntry.WorkspaceId, login, ct).ConfigureAwait(false); var resolution = new DatasetResolutionDTO { BICatalogId = catalogEntry.BICatalogId, Kind = catalogEntry.DatasetKind, IsShared = catalogEntry.TenantId == -1, WorkspaceId = catalogEntry.WorkspaceId }; // Computed/derived fields (MBIFIELDMAPPING.MAPPEDAS=2) are a post-resolution // layer common to ALL three DatasetKinds -- populated here, unconditionally, // BEFORE the kind switch, so no IDatasetResolver-specific branch needs to know // they exist. See AnalyticsBLL/AnalysisAggregation/ComputedFieldPlanner.cs. resolution.ComputedFields = await _fieldMappingDAL .GetComputedFieldsByCatalogId(catalogEntry.BICatalogId, login, ct).ConfigureAwait(false); switch (catalogEntry.DatasetKind) { case DatasetKind.Warehouse: await ResolveWarehouseAsync(resolution, catalogEntry, login, ct).ConfigureAwait(false); break; case DatasetKind.AnalysisQuery: await ResolveAnalysisQueryAsync(resolution, catalogEntry, login, ct).ConfigureAwait(false); break; case DatasetKind.ApiService: await ResolveApiServiceAsync(resolution, catalogEntry, login, ct).ConfigureAwait(false); break; default: throw new ArgumentException($"Unknown DatasetKind '{catalogEntry.DatasetKind}'.", nameof(biCatalogId)); } return resolution; } catch (Exception ex) { GB5Trace.MarkFailed("resolve-dataset-failed", ex); _logger.LogError(ex, "ResolveAsync failed for BICatalogId {Id}", biCatalogId); throw; } } private async Task ResolveWarehouseAsync(DatasetResolutionDTO resolution, BICatalogDTO catalogEntry, LoginDTO login, CancellationToken ct) { var fact = await _warehouseDAL.GetFactById(catalogEntry.SourceRefId, login, ct).ConfigureAwait(false); if (fact is null) throw new ArgumentException($"Warehouse fact {catalogEntry.SourceRefId} does not exist.", nameof(catalogEntry)); resolution.FactId = fact.FactId; resolution.FactTableName = fact.FactTableName; resolution.DateDimColumn = fact.DateDimColumn; resolution.OuDimColumn = fact.OuDimColumn; resolution.Measures = (await _warehouseDAL.GetMeasuresByFactId(fact.FactId, login, ct).ConfigureAwait(false)); } private async Task ResolveAnalysisQueryAsync(DatasetResolutionDTO resolution, BICatalogDTO catalogEntry, LoginDTO login, CancellationToken ct) { var queryInfo = await _analysisQueryDAL.GetAnalysisQueryTypeInfo(catalogEntry.SourceRefId, login, ct).ConfigureAwait(false); if (queryInfo is null) throw new ArgumentException($"AnalysisQuery {catalogEntry.SourceRefId} does not exist.", nameof(catalogEntry)); // Mitigation for the AnalysisDAL.ApplyTemplateSubstitution SQL-injection-adjacent gap // (see AnalysisDAL.cs's own "TODO: Route ... through SqlWorkbench safe executor" // comments): this unified endpoint only serves SELECTION_BASED queries by default. // Revisit once the safe-executor lands. if (queryInfo.QueryType == AnalysisQueryType.DIRECT) throw new InvalidOperationException( $"AnalysisQuery {catalogEntry.SourceRefId} is a DIRECT-type query, which RunBIQuery " + "does not resolve pending the SqlWorkbench safe-executor rewrite of AnalysisDAL.ApplyTemplateSubstitution."); resolution.AnalysisId = queryInfo.AnalysisId; resolution.AnalysisQueryId = catalogEntry.SourceRefId; resolution.ReportViewId = catalogEntry.ReportViewId; // Best-effort: resolve a per-tenant OverrideDataSourceId if this analysis is bound // into the catalog's workspace via MANALYSISWORKSPACE. Not consumed by // AnalysisDAL.DynamicOutput today (it has no datasource-routing parameter) — carried // on DatasetResolutionDTO for forward compatibility once that routing exists. if (catalogEntry.WorkspaceId != -1) { var json = await _analysisWorkspaceDAL.GetWorkspacesByAnalysis(queryInfo.AnalysisId, login, ct).ConfigureAwait(false); var bindings = JsonSerializer.Deserialize>(json) ?? new List(); resolution.ResolvedDataSourceId = bindings .FirstOrDefault(b => b.WorkspaceId == catalogEntry.WorkspaceId)?.OverrideDataSourceId; } } private async Task ResolveApiServiceAsync(DatasetResolutionDTO resolution, BICatalogDTO catalogEntry, LoginDTO login, CancellationToken ct) { await _dataSourceBLL.ValidateDataSourceVisibility(catalogEntry.SourceRefId, login, ct).ConfigureAwait(false); resolution.DataSourceId = catalogEntry.SourceRefId; resolution.ResourcePath = catalogEntry.ApiResourcePath ?? throw new InvalidOperationException($"Dataset {catalogEntry.BICatalogId} is ApiService-kind but has no ApiResourcePath configured."); resolution.RequiresInMemoryGrouping = true; // MBIFIELDMAPPING's measure rows — the server-side additivity/aggregation // registry ValidateAdditivity trusts instead of the caller-supplied definition, // mirroring how ResolveWarehouseAsync above populates resolution.Measures from // MWAREHOUSEMEASURE. resolution.ApiMeasures = await _fieldMappingDAL.GetMeasuresByCatalogId(catalogEntry.BICatalogId, login, ct).ConfigureAwait(false); } } }