309 lines
12 KiB
C#
309 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Sozsoft.Mcp.Dtos;
|
|
using Sozsoft.Mcp.Services;
|
|
using Sozsoft.Platform;
|
|
using Sozsoft.Platform.Data.Seeds;
|
|
using Sozsoft.Platform.Enums;
|
|
using Sozsoft.Platform.ListForms;
|
|
using Volo.Abp.Authorization.Permissions;
|
|
|
|
namespace Sozsoft.Mcp;
|
|
|
|
/// <summary>
|
|
/// Sema kesfini JSON veri sozlugu uzerinden karsilar; sozlugu okuma ve arama normalizasyonu
|
|
/// domain servisindedir (<see cref="IMcpSchemaDictionary"/>). Kullanici yalnizca uygulamada
|
|
/// okuma yetkisi olan listeleri gorur (<see cref="IMcpListFormAccessFilter"/>).
|
|
/// </summary>
|
|
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
|
|
public class McpSchemaAppService(
|
|
IMcpServerConfigProvider serverConfigProvider,
|
|
IMcpSchemaDictionary dictionary,
|
|
IMcpListFormAccessFilter accessFilter,
|
|
IPermissionChecker permissionChecker,
|
|
IListFormWizardAppService wizardAppService,
|
|
IConfiguration configuration)
|
|
: McpAppService(serverConfigProvider), IMcpSchemaAppService
|
|
{
|
|
/// <summary>Wizard dosyasi olan ama sozlukte olmayan ekran (DB Migrate bekliyor).</summary>
|
|
private const string PendingSourceKind = "pending";
|
|
|
|
/// <summary>Yalnizca seed scripti olan, ekrani olmayan tablo.</summary>
|
|
private const string PendingTableSourceKind = "pendingTable";
|
|
|
|
public async Task<McpServerConfigDto?> GetServerConfigAsync()
|
|
=> await ResolveServerConfigAsync();
|
|
|
|
public async Task<List<string>> GetAvailableToolsAsync()
|
|
{
|
|
var config = await ResolveServerConfigAsync();
|
|
var candidates = McpToolPolicy.VisibleTools(
|
|
config is not null,
|
|
CurrentTenant.IsAvailable,
|
|
config?.AllowedTools ?? []);
|
|
|
|
var granted = await permissionChecker.GetGrantedNamesAsync(candidates
|
|
.Where(McpToolNames.RequiredPermissions.ContainsKey)
|
|
.Select(tool => McpToolNames.RequiredPermissions[tool]));
|
|
|
|
return [.. candidates.Where(tool =>
|
|
!McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))];
|
|
}
|
|
|
|
public async Task<McpSchemaInfoDto> GetSchemaInfoAsync()
|
|
{
|
|
var snapshot = await dictionary.GetSnapshotAsync();
|
|
if (snapshot is null)
|
|
{
|
|
return new McpSchemaInfoDto
|
|
{
|
|
Available = false,
|
|
DictionaryKey = dictionary.ResolveTenantName(),
|
|
Message = L[McpErrorCodes.DictionaryNotFound, McpConsts.DictionaryProcedureName]
|
|
};
|
|
}
|
|
|
|
return new McpSchemaInfoDto
|
|
{
|
|
Available = true,
|
|
CacheKey = snapshot.CacheKey,
|
|
RefreshedAt = snapshot.RefreshedAt,
|
|
FormCount = snapshot.FormCount,
|
|
FieldCount = snapshot.FieldCount,
|
|
DictionaryKey = snapshot.TenantName,
|
|
DefinitionCultureName = snapshot.DefinitionCultureName,
|
|
UiCultureName = snapshot.UiCultureName
|
|
};
|
|
}
|
|
|
|
public async Task<McpOutcome<List<McpListSummaryDto>>> GetListsAsync(string? search = null, int maxResults = 200)
|
|
{
|
|
if (await EnsureToolAllowedAsync(McpToolNames.ListLists) is { Problem: { } problem })
|
|
{
|
|
return problem;
|
|
}
|
|
|
|
var forms = await GetReadableListFormsAsync();
|
|
var normalizedSearch = dictionary.Normalize(search);
|
|
|
|
var matches = forms.Where(form => normalizedSearch.Length == 0
|
|
|| dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal));
|
|
|
|
return matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary).ToList();
|
|
}
|
|
|
|
public async Task<McpOutcome<McpListDetailDto?>> GetListAsync(string listFormCode)
|
|
{
|
|
if (await EnsureToolAllowedAsync(McpToolNames.DescribeList) is { Problem: { } problem })
|
|
{
|
|
return problem;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(listFormCode))
|
|
{
|
|
return default(McpListDetailDto);
|
|
}
|
|
|
|
var forms = await GetReadableListFormsAsync();
|
|
var form = forms.FirstOrDefault(candidate =>
|
|
string.Equals(candidate.ListFormCode, listFormCode, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (form is null)
|
|
{
|
|
return default(McpListDetailDto);
|
|
}
|
|
|
|
var detail = new McpListDetailDto();
|
|
CopySummary(form, detail);
|
|
detail.Columns = [.. (form.Fields ?? []).Select(ToColumn)];
|
|
|
|
return detail;
|
|
}
|
|
|
|
public async Task<McpOutcome<List<McpSchemaMatchDto>>> SearchAsync(string term, int maxResults = 50)
|
|
{
|
|
if (await EnsureToolAllowedAsync(McpToolNames.SearchSchema) is { Problem: { } problem })
|
|
{
|
|
return problem;
|
|
}
|
|
|
|
var normalized = dictionary.Normalize(term);
|
|
if (normalized.Length == 0)
|
|
{
|
|
return new List<McpSchemaMatchDto>();
|
|
}
|
|
|
|
var limit = NormalizeLimit(maxResults, 50);
|
|
var forms = await GetReadableListFormsAsync();
|
|
var results = new List<McpSchemaMatchDto>();
|
|
|
|
foreach (var form in forms)
|
|
{
|
|
var formMatches = dictionary.Normalize(form.SearchText).Contains(normalized, StringComparison.Ordinal);
|
|
|
|
var columnMatches = (form.Fields ?? [])
|
|
.Where(field => dictionary.Normalize(field.Field).Contains(normalized, StringComparison.Ordinal)
|
|
|| dictionary.Normalize(field.DisplayName).Contains(normalized, StringComparison.Ordinal))
|
|
.Select(ToColumn)
|
|
.ToList();
|
|
|
|
if (!formMatches && columnMatches.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
results.Add(new McpSchemaMatchDto
|
|
{
|
|
ListFormCode = form.ListFormCode ?? string.Empty,
|
|
DisplayName = form.DisplayName,
|
|
TableName = NullIfEmpty(form.TableName),
|
|
MatchedColumns = columnMatches
|
|
});
|
|
|
|
if (results.Count >= limit)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private async Task<List<McpDictionaryListForm>> GetReadableListFormsAsync()
|
|
{
|
|
var forms = await dictionary.GetListFormsAsync();
|
|
var readable = await accessFilter.GetReadableListFormCodesAsync();
|
|
|
|
List<McpDictionaryListForm> result = [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))];
|
|
|
|
// Gelistirici, sozlukte henuz olmayan (DB Migrate bekleyen ya da sozluk tazelenmemis) ekran ve
|
|
// tablolari da gorur; lookup, widget ve view SQL'i bunlarin gercek kolon adlariyla yazilir.
|
|
if (!CurrentTenant.IsAvailable && await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizard))
|
|
result.AddRange(await GetSeedOnlySourcesAsync(forms));
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seed klasorunde olup sozlukte olmayan kaynaklar: wizard dosyalarinin ekranlari (alanlari
|
|
/// <c>Fields</c> bolumunden) ve yalnizca <c>sql/object</c> scripti olan tablolar (kolonlari scriptten).
|
|
/// <see cref="McpListSummaryDto.SourceKind"/> <c>pending</c> / <c>pendingTable</c> olarak isaretlenir:
|
|
/// kolon adlari dogrudur ama query_list migrate'ten once calismaz.
|
|
/// </summary>
|
|
private async Task<List<McpDictionaryListForm>> GetSeedOnlySourcesAsync(IReadOnlyList<McpDictionaryListForm> known)
|
|
{
|
|
var codes = known.Select(form => form.ListFormCode).OfType<string>().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
var tables = known.Select(form => form.TableName).OfType<string>().ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
var result = new List<McpDictionaryListForm>();
|
|
|
|
foreach (var file in await wizardAppService.GetFiles())
|
|
{
|
|
if (file.ComponentKind == WizardComponentKindEnum.Custom || codes.Contains(file.ListFormCode ?? string.Empty))
|
|
continue;
|
|
|
|
var seed = await wizardAppService.GetFile(file.FileName);
|
|
if (seed.ListForm is not { } listForm)
|
|
continue;
|
|
|
|
var table = listForm.SelectCommandType == SelectCommandTypeEnum.Query ? null : listForm.SelectCommand?.Split('.')[^1].Trim('[', ']', '"');
|
|
if (table is not null)
|
|
tables.Add(table);
|
|
|
|
result.Add(new McpDictionaryListForm
|
|
{
|
|
ListFormCode = listForm.ListFormCode,
|
|
DisplayName = seed.Wizard?.TitleTr ?? seed.Wizard?.TitleEn,
|
|
Title = seed.Wizard?.TitleEn,
|
|
Description = seed.Wizard?.DescTr,
|
|
TableName = table,
|
|
SqlSourceName = listForm.SelectCommand,
|
|
SourceKind = PendingSourceKind,
|
|
KeyFieldName = listForm.KeyFieldName,
|
|
SearchText = string.Join(' ', listForm.ListFormCode, seed.Wizard?.TitleTr, seed.Wizard?.TitleEn, table),
|
|
Fields = [.. seed.Fields.Select(field => new McpDictionaryField
|
|
{
|
|
Field = field.FieldName,
|
|
DisplayName = field.CaptionName,
|
|
SqlDataType = field.SourceDbType.ToString(),
|
|
IsVisible = field.Visible,
|
|
IsBooleanColumn = field.SourceDbType == DbType.Boolean,
|
|
LookupValueColumn = string.IsNullOrWhiteSpace(field.LookupJson) ? null : field.LookupDto.ValueExpr,
|
|
LookupDisplayColumn = string.IsNullOrWhiteSpace(field.LookupJson) ? null : field.LookupDto.DisplayExpr,
|
|
IsDefaultSelectColumn = field.Visible
|
|
})]
|
|
});
|
|
}
|
|
|
|
var scriptFolder = SeedPathResolver.GetPath(configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder);
|
|
foreach (var path in Directory.Exists(scriptFolder) ? Directory.GetFiles(scriptFolder, "*.sql") : [])
|
|
{
|
|
var table = Path.GetFileNameWithoutExtension(path);
|
|
if (tables.Contains(table))
|
|
continue;
|
|
|
|
var columns = PostgreSqlTableScript.ReadSeedColumns(configuration, table);
|
|
if (columns.Count == 0)
|
|
continue;
|
|
|
|
result.Add(new McpDictionaryListForm
|
|
{
|
|
ListFormCode = table,
|
|
TableName = table,
|
|
SqlSourceName = table,
|
|
SourceKind = PendingTableSourceKind,
|
|
KeyFieldName = columns.ContainsKey("Id") ? "Id" : null,
|
|
SearchText = table,
|
|
Fields = [.. columns.Keys.Select(column => new McpDictionaryField { Field = column, IsVisible = true, IsDefaultSelectColumn = true })]
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static McpListSummaryDto ToSummary(McpDictionaryListForm form)
|
|
{
|
|
var summary = new McpListSummaryDto();
|
|
CopySummary(form, summary);
|
|
|
|
return summary;
|
|
}
|
|
|
|
private static void CopySummary(McpDictionaryListForm form, McpListSummaryDto target)
|
|
{
|
|
target.ListFormCode = form.ListFormCode ?? string.Empty;
|
|
target.DisplayName = NullIfEmpty(form.DisplayName);
|
|
target.Title = NullIfEmpty(form.Title);
|
|
target.Description = NullIfEmpty(form.Description);
|
|
target.TableName = NullIfEmpty(form.TableName);
|
|
target.SqlSourceName = NullIfEmpty(form.SqlSourceName);
|
|
target.SourceKind = NullIfEmpty(form.SourceKind);
|
|
target.KeyFieldName = NullIfEmpty(form.KeyFieldName);
|
|
target.FieldCount = form.Fields?.Count ?? 0;
|
|
}
|
|
|
|
private static McpColumnDto ToColumn(McpDictionaryField field) => new()
|
|
{
|
|
Field = field.Field ?? string.Empty,
|
|
DisplayName = NullIfEmpty(field.DisplayName),
|
|
SqlDataType = NullIfEmpty(field.SqlDataType),
|
|
IsVisible = field.IsVisible,
|
|
IsBooleanColumn = field.IsBooleanColumn,
|
|
LookupTableName = NullIfEmpty(field.LookupTableName),
|
|
LookupValueColumn = NullIfEmpty(field.LookupValueColumn),
|
|
LookupDisplayColumn = NullIfEmpty(field.LookupDisplayColumn),
|
|
IsSensitive = field.IsSensitive,
|
|
IsDefaultSelectColumn = field.IsDefaultSelectColumn
|
|
};
|
|
|
|
private static int NormalizeLimit(int requested, int fallback)
|
|
=> requested <= 0 ? fallback : Math.Min(requested, fallback * 5);
|
|
|
|
private static string? NullIfEmpty(string? value)
|
|
=> string.IsNullOrWhiteSpace(value) ? null : value;
|
|
}
|