From c842bd6843eac12814cbd97872aa3b2de3211c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 24 Jun 2026 19:59:51 +0300 Subject: [PATCH] SaveDbTableColumns --- .../Seeds/HostData.json | 8 + .../Sas_H_ListFormFieldsDictionaryJson.sql | 362 ++++++++++++++++++ ...ieldsDictionaryJson_SaveDbTableColumns.sql | 153 ++++++++ .../Enums/TableNameEnum.cs | 1 + .../TableNameResolver.cs | 1 + .../Entities/Host/DbTableColumn.cs | 25 ++ .../EntityFrameworkCore/PlatformDbContext.cs | 26 ++ ....cs => 20260624135753_Initial.Designer.cs} | 101 ++++- ...7_Initial.cs => 20260624135753_Initial.cs} | 31 ++ .../PlatformDbContextModelSnapshot.cs | 99 +++++ configs/ai/Chat.json | 173 +++++---- ui/src/views/ai/Assistant.tsx | 15 +- 12 files changed, 907 insertions(+), 88 deletions(-) create mode 100644 api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson.sql create mode 100644 api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Host/DbTableColumn.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260623164647_Initial.Designer.cs => 20260624135753_Initial.Designer.cs} (98%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260623164647_Initial.cs => 20260624135753_Initial.cs} (99%) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/HostData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/HostData.json index b13f556..e165163 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/HostData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/HostData.json @@ -1142,6 +1142,14 @@ "dataSourceCode": "Default", "beforeSp": "Adm_T_DatabaseBackupAll", "afterSp": "Adm_T_DatabaseBackupFilesDeleteAll" + }, + { + "name": "AI Save DbTableColumns", + "cron": "0 3 * * *", + "workerType": "BackupWorker", + "isActive": true, + "dataSourceCode": "Default", + "beforeSp": "Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns" } ], "ContactTitles": [ diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson.sql b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson.sql new file mode 100644 index 0000000..99784d7 --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson.sql @@ -0,0 +1,362 @@ +USE [Sozsoft] +GO + +-- v12: Fully dynamic lookup dictionary. No hard-coded field-specific lookup rules. +CREATE OR ALTER PROCEDURE [dbo].[Sas_H_ListFormFieldsDictionaryJson] + @DefinitionCultureName NVARCHAR(10) = N'en', -- ListForm/ListFormField kayıt kültürü. Genelde en. + @UiCultureName NVARCHAR(10) = N'tr', -- Kullanıcıya görünen dil. Genelde tr. + @ListFormCode NVARCHAR(64) = NULL, -- Tek form istenirse. + @OnlyBaseDefinitions BIT = 1, -- 1 = UserId/RoleId özel kayıtları hariç. + @ResourceName NVARCHAR(64) = NULL -- Dil kaynağı filtrelenmek istenirse. +AS +BEGIN + SET NOCOUNT ON; + + ;WITH ListFormSource AS + ( + SELECT + L.Id, + L.ListFormCode, + L.CultureName, + L.Name, + NameValue = LT_NAME.[Value], + L.Title, + TitleValue = LT_TITLE.[Value], + L.Description, + DescriptionValue = LT_DESC.[Value], + L.TableName, + SelectCommandText = NULLIF(LTRIM(RTRIM(CAST(L.SelectCommand AS NVARCHAR(MAX)))), N''), + L.SelectCommandType, + L.KeyFieldName, + L.KeyFieldDbSourceType, + L.ListFormType, + L.PageSize, + L.SortMode, + L.IsTenant, + L.IsBranch, + L.IsOrganizationUnit, + L.IsSubForm, + L.UserId, + L.RoleId + FROM dbo.Sas_H_ListForm L + OUTER APPLY + ( + SELECT TOP (1) LT.[Value] + FROM dbo.Sas_H_LanguageText LT + WHERE LT.CultureName = @UiCultureName + AND LT.[Key] = L.Name + AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) + ORDER BY LT.ResourceName + ) LT_NAME + OUTER APPLY + ( + SELECT TOP (1) LT.[Value] + FROM dbo.Sas_H_LanguageText LT + WHERE LT.CultureName = @UiCultureName + AND LT.[Key] = L.Title + AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) + ORDER BY LT.ResourceName + ) LT_TITLE + OUTER APPLY + ( + SELECT TOP (1) LT.[Value] + FROM dbo.Sas_H_LanguageText LT + WHERE LT.CultureName = @UiCultureName + AND LT.[Key] = L.Description + AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) + ORDER BY LT.ResourceName + ) LT_DESC + WHERE + (@DefinitionCultureName IS NULL OR L.CultureName = @DefinitionCultureName) + AND (@ListFormCode IS NULL OR L.ListFormCode = @ListFormCode) + AND + ( + @OnlyBaseDefinitions = 0 + OR (L.UserId IS NULL AND L.RoleId IS NULL) + ) + ) + SELECT + ( + SELECT + L.ListFormCode AS [formCode], + L.ListFormCode AS [listFormCode], + L.CultureName AS [definitionCulture], + @UiCultureName AS [uiCulture], + + ISNULL(CAST(L.NameValue AS NVARCHAR(MAX)), ISNULL(L.Name, N'')) AS [displayName], + ISNULL(CAST(L.NameValue AS NVARCHAR(MAX)), ISNULL(L.Name, N'')) AS [nameValue], + ISNULL(CAST(L.TitleValue AS NVARCHAR(MAX)), ISNULL(L.Title, N'')) AS [title], + ISNULL(CAST(L.TitleValue AS NVARCHAR(MAX)), ISNULL(L.Title, N'')) AS [titleValue], + ISNULL(CAST(L.DescriptionValue AS NVARCHAR(MAX)), ISNULL(L.Description, N'')) AS [description], + ISNULL(CAST(L.DescriptionValue AS NVARCHAR(MAX)), ISNULL(L.Description, N'')) AS [descriptionValue], + + ISNULL(L.TableName, N'') AS [sqlTableName], + ISNULL(L.TableName, N'') AS [tableName], + ISNULL(L.SelectCommandText, N'') AS [selectCommand], + ISNULL(COALESCE(NULLIF(L.TableName, N''), L.SelectCommandText), N'') AS [sqlSourceName], + CASE + WHEN COALESCE(NULLIF(L.TableName, N''), L.SelectCommandText) IS NULL THEN N'unknown' + WHEN UPPER(LTRIM(COALESCE(NULLIF(L.TableName, N''), L.SelectCommandText))) LIKE N'SELECT%' THEN N'selectSql' + ELSE N'table' + END AS [sourceKind], + L.SelectCommandType AS [selectCommandType], + + ISNULL(L.KeyFieldName, N'') AS [primaryKeyColumn], + ISNULL(L.KeyFieldName, N'') AS [keyFieldName], + L.KeyFieldDbSourceType AS [keyFieldDbSourceType], + L.ListFormType AS [listFormType], + L.PageSize AS [pageSize], + L.SortMode AS [sortMode], + CONVERT(bit, L.IsTenant) AS [isTenant], + CONVERT(bit, L.IsBranch) AS [isBranch], + CONVERT(bit, L.IsOrganizationUnit) AS [isOrganizationUnit], + CONVERT(bit, L.IsSubForm) AS [isSubForm], + + CONCAT_WS(N' ', + L.ListFormCode, + L.TableName, + L.SelectCommandText, + ISNULL(CAST(L.NameValue AS NVARCHAR(MAX)), L.Name), + ISNULL(CAST(L.TitleValue AS NVARCHAR(MAX)), L.Title), + ISNULL(CAST(L.DescriptionValue AS NVARCHAR(MAX)), L.Description) + ) AS [searchText], + + JSON_QUERY + ( + ( + SELECT + F.FieldName AS [sqlColumnName], + F.FieldName AS [field], + ISNULL(CAST(LT_CAP.[Value] AS NVARCHAR(MAX)), ISNULL(F.CaptionName, N'')) AS [displayName], + ISNULL(CAST(LT_CAP.[Value] AS NVARCHAR(MAX)), ISNULL(F.CaptionName, N'')) AS [captionValue], + ISNULL(F.CaptionName, N'') AS [captionKey], + F.SourceDbType AS [sourceDbType], + SC.SqlDataType AS [sqlDataType], + SC.SqlMaxLength AS [sqlMaxLength], + CONVERT(bit, CASE + WHEN SC.SqlDataType = N'bit' THEN 1 + WHEN F.FieldName LIKE N'Is%' THEN 1 + WHEN F.FieldName LIKE N'Has%' THEN 1 + WHEN F.FieldName IN (N'Monday', N'Tuesday', N'Wednesday', N'Thursday', N'Friday', N'Saturday', N'Sunday') THEN 1 + WHEN F.FieldName IN (N'EmailConfirmed', N'TwoFactorEnabled', N'IsVerified', N'IsDefault', N'IsPublic', N'IsEnabled', N'IsPublished', N'IsPinned', N'IsAnonymous', N'IsFixed', N'IsCustomized', N'IsEncrypted', N'IsInherited', N'ZipRequired', N'StateRequired') THEN 1 + ELSE 0 + END) AS [isBooleanColumn], + F.ListOrderNo AS [listOrderNo], + F.Width AS [width], + F.Alignment AS [alignment], + F.Format AS [format], + CONVERT(bit, ISNULL(F.Visible, 1)) AS [isVisible], + CONVERT(bit, ISNULL(F.IsActive, 1)) AS [isActive], + CONVERT(bit, ISNULL(F.AllowSearch, 0)) AS [allowSearch], + CONVERT(bit, ISNULL(F.AllowEditing, 1)) AS [allowEditing], + CONVERT(bit, ISNULL(F.AllowAdding, 1)) AS [allowAdding], + + CONVERT(bit, CASE WHEN LJ.LookupJsonText IS NOT NULL THEN 1 ELSE 0 END) AS [hasLookup], + CONVERT(bit, CASE WHEN LJ.LookupJsonText IS NOT NULL THEN 1 ELSE 0 END) AS [isLookupColumn], + LJ.LookupJsonText AS [lookupJsonText], + JSON_QUERY(CASE WHEN ISJSON(LJ.LookupJsonText) = 1 THEN LJ.LookupJsonText ELSE NULL END) AS [lookupJson], + LJ.JoinTableJsonText AS [joinTableJsonText], + JSON_QUERY(CASE WHEN ISJSON(LJ.JoinTableJsonText) = 1 THEN LJ.JoinTableJsonText ELSE NULL END) AS [joinTableJson], + LKE.ParsedLookupTableName AS [parsedLookupTableName], + LK.ParsedLookupValueColumn AS [parsedLookupValueColumn], + LK.ParsedLookupDisplayColumn AS [parsedLookupDisplayColumn], + + CASE WHEN ISJSON(LJ.LookupJsonText) = 1 THEN COALESCE( + JSON_VALUE(LJ.LookupJsonText, '$.listFormCode'), + JSON_VALUE(LJ.LookupJsonText, '$.formCode'), + JSON_VALUE(LJ.LookupJsonText, '$.lookupListFormCode'), + JSON_VALUE(LJ.LookupJsonText, '$.dataSource.listFormCode'), + JSON_VALUE(LJ.LookupJsonText, '$.dataSource.formCode') + ) END AS [lookupListFormCode], + COALESCE( + CASE WHEN ISJSON(LJ.LookupJsonText) = 1 THEN COALESCE( + JSON_VALUE(LJ.LookupJsonText, '$.tableName'), + JSON_VALUE(LJ.LookupJsonText, '$.sqlTableName'), + JSON_VALUE(LJ.LookupJsonText, '$.lookupTableName'), + JSON_VALUE(LJ.LookupJsonText, '$.dataSource.tableName'), + JSON_VALUE(LJ.LookupJsonText, '$.dataSource.sqlTableName') + ) END, + LKE.ParsedLookupTableName + ) AS [lookupTableName], + COALESCE( + CASE WHEN ISJSON(LJ.LookupJsonText) = 1 THEN COALESCE( + JSON_VALUE(LJ.LookupJsonText, '$.valueExpr'), + JSON_VALUE(LJ.LookupJsonText, '$.valueField'), + JSON_VALUE(LJ.LookupJsonText, '$.valueColumn'), + JSON_VALUE(LJ.LookupJsonText, '$.keyField'), + JSON_VALUE(LJ.LookupJsonText, '$.keyFieldName') + ) END, + LK.ParsedLookupValueColumn + ) AS [lookupValueColumn], + COALESCE( + CASE WHEN ISJSON(LJ.LookupJsonText) = 1 THEN COALESCE( + JSON_VALUE(LJ.LookupJsonText, '$.displayExpr'), + JSON_VALUE(LJ.LookupJsonText, '$.displayField'), + JSON_VALUE(LJ.LookupJsonText, '$.displayColumn'), + JSON_VALUE(LJ.LookupJsonText, '$.textField'), + JSON_VALUE(LJ.LookupJsonText, '$.textColumn'), + JSON_VALUE(LJ.LookupJsonText, '$.nameField'), + JSON_VALUE(LJ.LookupJsonText, '$.nameColumn') + ) END, + LK.ParsedLookupDisplayColumn + ) AS [lookupDisplayColumn], + CASE + WHEN LJ.LookupJsonText IS NOT NULL AND F.FieldName NOT LIKE N'%Id' AND COALESCE(LK.ParsedLookupValueColumn, N'') IN (N'Name', N'Code', N'Key', N'Value') THEN N'direct-string' + WHEN LJ.LookupJsonText IS NOT NULL OR F.FieldName LIKE N'%Id' THEN N'convert-nvarchar-safe' + END AS [lookupJoinMode], + CASE + WHEN LJ.LookupJsonText IS NOT NULL AND F.FieldName NOT LIKE N'%Id' AND COALESCE(LK.ParsedLookupValueColumn, N'') IN (N'Name', N'Code', N'Key', N'Value') THEN N'ON base.lookupColumn = lookup.valueColumn' + WHEN LJ.LookupJsonText IS NOT NULL OR F.FieldName LIKE N'%Id' THEN N'ON CONVERT(NVARCHAR(100), base.lookupColumn) = CONVERT(NVARCHAR(100), lookup.valueColumn)' + END AS [lookupJoinRule], + CONVERT(bit, CASE + WHEN LJ.LookupJsonText IS NOT NULL + AND F.FieldName NOT LIKE N'%Id' + THEN 1 ELSE 0 END) AS [lookupFallbackToSourceColumn], + CASE WHEN LJ.LookupJsonText IS NOT NULL THEN CONCAT_WS(N' ', F.FieldName, F.CaptionName, CAST(LT_CAP.[Value] AS NVARCHAR(MAX)), LJ.LookupJsonText, LJ.JoinTableJsonText) END AS [lookupSearchText], + + CONVERT(bit, CASE WHEN F.FieldName IN (N'IsActive', N'IsEnabled', N'IsPublished') THEN 1 ELSE 0 END) AS [isActiveStatusColumn], + CONVERT(bit, CASE WHEN F.FieldName IN (N'IsDeleted', N'Deleted', N'IsRemoved') THEN 1 ELSE 0 END) AS [isDeletedStatusColumn], + CONVERT(bit, CASE + WHEN F.FieldName LIKE N'%Password%' + OR F.FieldName LIKE N'%ConnectionString%' + OR F.FieldName LIKE N'%Token%' + OR F.FieldName LIKE N'%Secret%' + OR F.FieldName LIKE N'%ApiKey%' + OR F.FieldName LIKE N'%PrivateKey%' + THEN 1 ELSE 0 END) AS [isSensitive], + CONVERT(bit, CASE + WHEN ISNULL(F.Visible, 1) = 1 + AND ISNULL(F.IsActive, 1) = 1 + AND F.FieldName NOT LIKE N'%Password%' + AND F.FieldName NOT LIKE N'%ConnectionString%' + AND F.FieldName NOT LIKE N'%Token%' + AND F.FieldName NOT LIKE N'%Secret%' + AND F.FieldName NOT LIKE N'%ApiKey%' + AND F.FieldName NOT LIKE N'%PrivateKey%' + AND F.FieldName NOT IN (N'Avatar', N'ExtraProperties') + THEN 1 ELSE 0 END) AS [isDefaultSelectColumn] + FROM dbo.Sas_H_ListFormField F + OUTER APPLY + ( + SELECT + LookupJsonText = NULLIF(LTRIM(RTRIM(CAST(F.LookupJson AS NVARCHAR(MAX)))), N''), + JoinTableJsonText = NULLIF(LTRIM(RTRIM(CAST(F.JoinTableJson AS NVARCHAR(MAX)))), N'') + ) LJ + OUTER APPLY + ( + SELECT TOP (1) + SqlDataType = TYPE_NAME(C.user_type_id), + SqlMaxLength = C.max_length, + SqlPrecision = C.precision, + SqlScale = C.scale + FROM sys.schemas S + INNER JOIN sys.tables TBL ON TBL.schema_id = S.schema_id + INNER JOIN sys.columns C ON C.object_id = TBL.object_id + WHERE S.name = N'dbo' + AND TBL.name = L.TableName + AND C.name = F.FieldName + ) SC + OUTER APPLY + ( + SELECT + CleanLookupSql = NULLIF( + LTRIM(RTRIM( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE( + REPLACE(CAST(F.LookupJson AS NVARCHAR(MAX)), N'\\u0022', N''), + N'\u0022', N''), + N'"', N''), + N'"', N''), + N'[', N''), + N']', N''), + N'`', N''), + N'\', N''), + CHAR(13) + CHAR(10), N' ') + )), + N'') + ) LKC + OUTER APPLY + ( + SELECT + UpperLookupSql = UPPER(LKC.CleanLookupSql), + FromPos = CHARINDEX(N' FROM ', UPPER(LKC.CleanLookupSql)), + OrderPos = CHARINDEX(N' ORDER BY ', UPPER(LKC.CleanLookupSql)), + WherePos = CHARINDEX(N' WHERE ', UPPER(LKC.CleanLookupSql)), + GroupPos = CHARINDEX(N' GROUP BY ', UPPER(LKC.CleanLookupSql)) + ) LKP + OUTER APPLY + ( + SELECT + StopPos = CASE + WHEN LKP.FromPos <= 0 THEN 0 + WHEN LKP.WherePos > LKP.FromPos THEN LKP.WherePos + WHEN LKP.GroupPos > LKP.FromPos THEN LKP.GroupPos + WHEN LKP.OrderPos > LKP.FromPos THEN LKP.OrderPos + ELSE LEN(LKC.CleanLookupSql) + 1 + END, + SelectPart = CASE WHEN LKP.FromPos > 0 THEN LTRIM(RTRIM(SUBSTRING(LKC.CleanLookupSql, 7, LKP.FromPos - 7))) END + ) LKS + OUTER APPLY + ( + SELECT + TablePart = CASE WHEN LKP.FromPos > 0 AND LKS.StopPos > LKP.FromPos + THEN LTRIM(RTRIM(SUBSTRING(LKC.CleanLookupSql, LKP.FromPos + 6, LKS.StopPos - (LKP.FromPos + 6)))) + END, + KeyAsPos = CHARINDEX(N' AS KEY', UPPER(ISNULL(LKS.SelectPart, N''))), + NameAsPos = CHARINDEX(N' AS NAME', UPPER(ISNULL(LKS.SelectPart, N''))) + ) LKT + OUTER APPLY + ( + SELECT + ParsedLookupTableName = NULLIF(REPLACE(REPLACE(LEFT(LKT.TablePart, CHARINDEX(N' ', LKT.TablePart + N' ') - 1), N'dbo.', N''), N'DBO.', N''), N''), + KeyExprChunk = CASE WHEN LKT.KeyAsPos > 0 THEN LTRIM(RTRIM(SUBSTRING(LKS.SelectPart, 1, LKT.KeyAsPos - 1))) END, + NameExprChunk = CASE WHEN LKT.NameAsPos > 0 THEN LTRIM(RTRIM(SUBSTRING(LKS.SelectPart, 1, LKT.NameAsPos - 1))) END + ) LKE + OUTER APPLY + ( + SELECT + ParsedLookupValueColumn = NULLIF(LTRIM(RTRIM(REPLACE(REPLACE( + CASE WHEN LKE.KeyExprChunk IS NOT NULL THEN + RIGHT(LKE.KeyExprChunk, CHARINDEX(N',', REVERSE(LKE.KeyExprChunk) + N',') - 1) + END, N'SELECT', N''), N'select', N''))), N''), + ParsedLookupDisplayColumn = NULLIF(LTRIM(RTRIM(REPLACE(REPLACE( + CASE WHEN LKE.NameExprChunk IS NOT NULL THEN + RIGHT(LKE.NameExprChunk, CHARINDEX(N',', REVERSE(LKE.NameExprChunk) + N',') - 1) + END, N'SELECT', N''), N'select', N''))), N'') + ) LK + OUTER APPLY + ( + SELECT TOP (1) LT.[Value] + FROM dbo.Sas_H_LanguageText LT + WHERE LT.CultureName = @UiCultureName + AND LT.[Key] = F.CaptionName + AND (@ResourceName IS NULL OR LT.ResourceName = @ResourceName) + ORDER BY LT.ResourceName + ) LT_CAP + WHERE + F.ListFormCode = L.ListFormCode + AND F.CultureName = L.CultureName + AND (@DefinitionCultureName IS NULL OR F.CultureName = @DefinitionCultureName) + AND + ( + @OnlyBaseDefinitions = 0 + OR (F.UserId IS NULL AND F.RoleId IS NULL) + ) + ORDER BY + ISNULL(F.ListOrderNo, 999999), + F.FieldName + FOR JSON PATH + ) + ) AS [fields] + FROM ListFormSource L + ORDER BY L.ListFormCode + FOR JSON PATH, ROOT('listForms') + ) AS ListFormDataDictionaryJson; +END +GO \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql new file mode 100644 index 0000000..49c07c9 --- /dev/null +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/SqlData/HostData/Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns.sql @@ -0,0 +1,153 @@ +USE [Sozsoft] +GO + +CREATE OR ALTER PROCEDURE [dbo].[Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns] + @TenantName NVARCHAR(128) = N'Sozsoft', + @DefinitionCultureName NVARCHAR(10) = N'en', + @UiCultureName NVARCHAR(10) = N'tr', + @ListFormCode NVARCHAR(64) = NULL, + @OnlyBaseDefinitions BIT = 1, + @ResourceName NVARCHAR(64) = NULL, + @MaxForms INT = 500, + @DeactivatePrevious BIT = 1, + @Note NVARCHAR(4000) = NULL +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF OBJECT_ID(N'dbo.Sas_H_ListFormFieldsDictionaryJson', N'P') IS NULL + BEGIN + THROW 51000, 'dbo.Sas_H_ListFormFieldsDictionaryJson bulunamadı. Önce mevcut JSON üreten SP kurulmalıdır.', 1; + END; + + DECLARE @Result TABLE + ( + ListFormDataDictionaryJson NVARCHAR(MAX) NULL + ); + + INSERT INTO @Result (ListFormDataDictionaryJson) + EXEC dbo.Sas_H_ListFormFieldsDictionaryJson + @DefinitionCultureName = @DefinitionCultureName, + @UiCultureName = @UiCultureName, + @ListFormCode = @ListFormCode, + @OnlyBaseDefinitions = @OnlyBaseDefinitions, + @ResourceName = @ResourceName; + + DECLARE @JsonData NVARCHAR(MAX); + + SELECT TOP (1) + @JsonData = ListFormDataDictionaryJson + FROM @Result + WHERE NULLIF(LTRIM(RTRIM(ListFormDataDictionaryJson)), N'') IS NOT NULL; + + IF @JsonData IS NULL + BEGIN + THROW 51001, 'JSON veri sözlüğü boş döndü. dbo.Sas_H_ListFormFieldsDictionaryJson çıktısını kontrol edin.', 1; + END; + + IF ISJSON(@JsonData) <> 1 + BEGIN + THROW 51002, 'Üretilen JsonData geçerli JSON değil.', 1; + END; + + DECLARE @CacheKey NVARCHAR(512) = CONCAT( + N'ListFormJson|', + ISNULL(@TenantName, N''), N'|', + ISNULL(@DefinitionCultureName, N''), N'|', + ISNULL(@UiCultureName, N''), N'|', + ISNULL(@ListFormCode, N''), N'|', + CONVERT(NVARCHAR(1), ISNULL(@OnlyBaseDefinitions, 1)), N'|', + ISNULL(@ResourceName, N''), N'|', + CONVERT(NVARCHAR(20), ISNULL(@MaxForms, 500)) + ); + + DECLARE @FormCount INT = 0; + DECLARE @FieldCount INT = 0; + DECLARE @JsonLength INT = LEN(@JsonData); + + SELECT @FormCount = COUNT(1) + FROM OPENJSON(@JsonData, '$.listForms'); + + SELECT @FieldCount = COUNT(1) + FROM OPENJSON(@JsonData, '$.listForms') AS LF + CROSS APPLY OPENJSON(JSON_QUERY(LF.[value], '$.fields')) AS FF; + + BEGIN TRAN; + + IF @DeactivatePrevious = 1 + BEGIN + UPDATE dbo.Sas_H_DbTableColumn + SET IsActive = 0 + WHERE DictionaryType = N'ListFormJson' + AND CacheKey = @CacheKey + AND ISNULL(IsActive, 0) = 1; + END; + + INSERT INTO dbo.Sas_H_DbTableColumn + ( + TenantName, + DictionaryType, + DefinitionCultureName, + UiCultureName, + ListFormCode, + OnlyBaseDefinitions, + ResourceName, + MaxForms, + CacheKey, + JsonData, + FormCount, + FieldCount, + JsonLength, + IsActive, + RefreshedAt, + SourceProcedure, + Note + ) + VALUES + ( + ISNULL(@TenantName, N'Sozsoft'), + N'ListFormJson', + ISNULL(@DefinitionCultureName, N'en'), + ISNULL(@UiCultureName, N'tr'), + @ListFormCode, + ISNULL(@OnlyBaseDefinitions, 1), + @ResourceName, + ISNULL(@MaxForms, 500), + @CacheKey, + @JsonData, + @FormCount, + @FieldCount, + @JsonLength, + 1, + SYSDATETIME(), + N'dbo.Sas_H_ListFormFieldsDictionaryJson', + @Note + ); + + COMMIT; + + SELECT TOP (1) + TenantName, + DictionaryType, + DefinitionCultureName, + UiCultureName, + ListFormCode, + OnlyBaseDefinitions, + ResourceName, + MaxForms, + CacheKey, + FormCount, + FieldCount, + JsonLength, + IsActive, + RefreshedAt, + SourceProcedure, + Note + FROM dbo.Sas_H_DbTableColumn + WHERE DictionaryType = N'ListFormJson' + AND CacheKey = @CacheKey + AND IsActive = 1 + ORDER BY RefreshedAt DESC; +END +GO \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs index 7a6bcb8..04b8d05 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs @@ -94,4 +94,5 @@ public enum TableNameEnum VideoroomAttandance, VideoroomChat, Sequence, + DbTableColumn } diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index c0262a6..01be3e7 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -12,6 +12,7 @@ public static class TableNameResolver _map = new(StringComparer.OrdinalIgnoreCase) { // 🔹 SAAS TABLOLARI + { nameof(TableNameEnum.DbTableColumn), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.LogEntry), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.AiBot), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.GlobalSearch), (TablePrefix.TenantByName, MenuPrefix.Saas) }, diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Host/DbTableColumn.cs b/api/src/Sozsoft.Platform.Domain/Entities/Host/DbTableColumn.cs new file mode 100644 index 0000000..aab953a --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Host/DbTableColumn.cs @@ -0,0 +1,25 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Sozsoft.Platform.Entities; + +public class DbTableColumn : Entity +{ + public string TenantName { get; set; } + public string DictionaryType { get; set; } + public string DefinitionCultureName { get; set; } + public string UiCultureName { get; set; } + public string? ListFormCode { get; set; } + public bool OnlyBaseDefinitions { get; set; } + public string? ResourceName { get; set; } + public int MaxForms { get; set; } + public string CacheKey { get; set; } + public string JsonData { get; set; } + public int FormCount { get; set; } + public int FieldCount { get; set; } + public int JsonLength { get; set; } + public bool IsActive { get; set; } + public DateTime RefreshedAt { get; set; } + public string? SourceProcedure { get; set; } + public string? Note { get; set; } +} \ No newline at end of file diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index b0ec2bd..924b960 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -41,6 +41,7 @@ public class PlatformDbContext : private readonly bool _isPostgreSql; #region Saas + public DbSet DbTableColumns { get; set; } public DbSet LogEntries { get; set; } public DbSet Tenants { get; set; } public DbSet TenantConnectionStrings { get; set; } @@ -1526,6 +1527,31 @@ public class PlatformDbContext : b.HasIndex(x => new { x.TenantId, x.Name }).IsUnique().HasFilter("[IsDeleted] = 0"); }); + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.DbTableColumn)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.Id).HasDefaultValueSql("NEWID()"); + b.Property(x => x.TenantName).HasMaxLength(128).IsRequired().HasDefaultValue("Sozsoft"); + b.Property(x => x.DictionaryType).HasMaxLength(50).IsRequired().HasDefaultValue("ListFormJson"); + b.Property(x => x.DefinitionCultureName).HasMaxLength(10).IsRequired().HasDefaultValue("en"); + b.Property(x => x.UiCultureName).HasMaxLength(10).IsRequired().HasDefaultValue("tr"); + b.Property(x => x.ListFormCode).HasMaxLength(64); + b.Property(x => x.OnlyBaseDefinitions).HasDefaultValue(true); + b.Property(x => x.ResourceName).HasMaxLength(64); + b.Property(x => x.MaxForms).HasDefaultValue(500); + b.Property(x => x.CacheKey).HasMaxLength(512).IsRequired(); + b.Property(x => x.JsonData).IsRequired(); + b.Property(x => x.FormCount).HasDefaultValue(0); + b.Property(x => x.FieldCount).HasDefaultValue(0); + b.Property(x => x.JsonLength).HasDefaultValue(0); + b.Property(x => x.IsActive).HasDefaultValue(true); + b.Property(x => x.RefreshedAt).HasDefaultValueSql("SYSDATETIME()"); + b.Property(x => x.SourceProcedure).HasMaxLength(128); + b.Property(x => x.Note).HasMaxLength(4000); + }); + ConfigureProviderSpecificModel(builder); } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260623164647_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260624135753_Initial.Designer.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260623164647_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260624135753_Initial.Designer.cs index 7b158db..55ea5d6 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260623164647_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260624135753_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260623164647_Initial")] + [Migration("20260624135753_Initial")] partial class Initial { /// @@ -1963,6 +1963,105 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Sas_H_DataSource", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.DbTableColumn", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("DefinitionCultureName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasDefaultValue("en"); + + b.Property("DictionaryType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasDefaultValue("ListFormJson"); + + b.Property("FieldCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("FormCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JsonLength") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ListFormCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("MaxForms") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(500); + + b.Property("Note") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("OnlyBaseDefinitions") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("RefreshedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSDATETIME()"); + + b.Property("ResourceName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("SourceProcedure") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TenantName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasDefaultValue("Sozsoft"); + + b.Property("UiCultureName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasDefaultValue("tr"); + + b.HasKey("Id"); + + b.ToTable("Sas_H_DbTableColumn", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Demo", b => { b.Property("Id") diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260623164647_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260624135753_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260623164647_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260624135753_Initial.cs index 871283a..688f2aa 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260623164647_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260624135753_Initial.cs @@ -1195,6 +1195,34 @@ namespace Sozsoft.Platform.Migrations table.PrimaryKey("PK_Sas_H_DataSource", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Sas_H_DbTableColumn", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWID()"), + TenantName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false, defaultValue: "Sozsoft"), + DictionaryType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false, defaultValue: "ListFormJson"), + DefinitionCultureName = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false, defaultValue: "en"), + UiCultureName = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false, defaultValue: "tr"), + ListFormCode = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + OnlyBaseDefinitions = table.Column(type: "bit", nullable: false, defaultValue: true), + ResourceName = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + MaxForms = table.Column(type: "int", nullable: false, defaultValue: 500), + CacheKey = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false), + JsonData = table.Column(type: "nvarchar(max)", nullable: false), + FormCount = table.Column(type: "int", nullable: false, defaultValue: 0), + FieldCount = table.Column(type: "int", nullable: false, defaultValue: 0), + JsonLength = table.Column(type: "int", nullable: false, defaultValue: 0), + IsActive = table.Column(type: "bit", nullable: false, defaultValue: true), + RefreshedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "SYSDATETIME()"), + SourceProcedure = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + Note = table.Column(type: "nvarchar(4000)", maxLength: 4000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sas_H_DbTableColumn", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Sas_H_Demo", columns: table => new @@ -4389,6 +4417,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Sas_H_DataSource"); + migrationBuilder.DropTable( + name: "Sas_H_DbTableColumn"); + migrationBuilder.DropTable( name: "Sas_H_Demo"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index f3d53fd..7874d47 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -1960,6 +1960,105 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Sas_H_DataSource", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.DbTableColumn", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("DefinitionCultureName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasDefaultValue("en"); + + b.Property("DictionaryType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasDefaultValue("ListFormJson"); + + b.Property("FieldCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("FormCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("JsonLength") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ListFormCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("MaxForms") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(500); + + b.Property("Note") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("OnlyBaseDefinitions") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("RefreshedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("SYSDATETIME()"); + + b.Property("ResourceName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("SourceProcedure") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TenantName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasDefaultValue("Sozsoft"); + + b.Property("UiCultureName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasDefaultValue("tr"); + + b.HasKey("Id"); + + b.ToTable("Sas_H_DbTableColumn", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Demo", b => { b.Property("Id") diff --git a/configs/ai/Chat.json b/configs/ai/Chat.json index a40d551..d12d32d 100644 --- a/configs/ai/Chat.json +++ b/configs/ai/Chat.json @@ -1,5 +1,5 @@ { - "name": "Chat SQL DataDictionary HTTP Stable v4", + "name": "Chat_SQL_DbTableColumns_TableAgent_v29_AliasFix_FullyDynamic", "nodes": [ { "parameters": { @@ -15,23 +15,24 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ - -900, - 200 + -32912, + -3184 ], - "id": "9f56ce73-26a2-4429-a77d-bfc6a91fe5d7", - "name": "Webhook" + "id": "18507856-9247-491e-8d4a-9159419d252e", + "name": "Webhook", + "webhookId": "b0b9c7f7-9eab-4227-8673-85e604bab2d2" }, { "parameters": { - "jsCode": "const question = $('Webhook').first().json.body?.question || '';\nreturn [{ json: { question, ollamaBody: { model: 'qwen2.5:3b', stream: false, options: { temperature: 0, top_p: 0.1, num_predict: 10 }, messages: [ { role: 'system', content: `Sen bir niyet sınıflandırma motorusun. Sadece CHAT veya SQL döndür. Kullanıcı veritabanından veri istiyorsa, listele/getir/say/toplam/raporla/sorgula/ara/filtrele gibi amaç varsa SQL döndür. Selamlaşma, açıklama, yardım, teşekkür, genel sohbet ise CHAT döndür. Açıklama yazma.` }, { role: 'user', content: question } ] } } }];" + "jsCode": "\nconst body = $('Webhook').first().json.body || {};\nconst question = String(body.question || body.message || '').trim();\nreturn [{ json: {\n question,\n ollamaBody: {\n model: body.intentModel || 'qwen2.5:3b',\n stream: false,\n options: { temperature: 0, top_p: 0.1, num_predict: 12 },\n messages: [\n { role: 'system', content: 'Sen bir niyet sınıflandırma motorusun. Sadece CHAT veya SQL döndür. Kullanıcı veritabanından veri istiyorsa, listele/getir/say/toplam/raporla/sorgula/ara/filtrele gibi amaç varsa SQL döndür. Selamlaşma, açıklama, yardım, teşekkür, genel sohbet ise CHAT döndür. Açıklama yazma.' },\n { role: 'user', content: question }\n ]\n }\n}}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -680, - 200 + -32704, + -3184 ], - "id": "e19a29c3-5833-4151-bd9c-d6c4cb49d2b0", + "id": "6067108d-b5dd-48df-9500-70416053db9f", "name": "Build Intent Request" }, { @@ -46,23 +47,23 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - -470, - 200 + -32480, + -3184 ], - "id": "2fddcd65-a682-4079-bbe1-67d63873d500", + "id": "3baceaba-e945-47cd-b8c9-6561125f260d", "name": "Intent Ollama" }, { "parameters": { - "jsCode": "const res = $input.first().json || {};\nconst content = String(res.message?.content || res.response || res.output || '').toUpperCase();\nconst question = $('Webhook').first().json.body?.question || '';\nconst tenantName = $('Webhook').first().json.body?.tenantName || 'Sozsoft';\nconst intent = content.includes('SQL') ? 'SQL' : 'CHAT';\nreturn [{ json: { question, tenantName, intent, intentRaw: res } }];" + "jsCode": "\nconst res = $input.first().json || {};\nconst body = $('Webhook').first().json.body || {};\nconst content = String(res.message?.content || res.response || res.output || '').toUpperCase();\nconst question = String(body.question || body.message || '').trim();\nconst tenantName = String(body.tenantName || 'Sozsoft').trim();\nconst intent = content.includes('SQL') ? 'SQL' : 'CHAT';\nreturn [{ json: { question, tenantName, intent, intentRaw: res } }];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -250, - 200 + -32256, + -3184 ], - "id": "adb78e6c-97b9-4a9a-8914-c2a4d935893e", + "id": "d4e9ab81-138e-4acc-a31d-e18be333d9ec", "name": "Normalize Intent" }, { @@ -72,11 +73,11 @@ "caseSensitive": true, "leftValue": "", "typeValidation": "strict", - "version": 2 + "version": 1 }, "conditions": [ { - "id": "989bc03a-0479-4cee-a071-b2f991c5def5", + "id": "89f40b61-7147-4055-8ffb-5e24ad9f7879", "leftValue": "={{ $json.intent }}", "rightValue": "SQL", "operator": { @@ -92,23 +93,23 @@ "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ - -40, - 200 + -32032, + -3184 ], - "id": "0221a195-1c08-47be-adab-1539dddce98b", + "id": "0ae28c72-73f3-4851-b149-d4a2913418a9", "name": "Is SQL?" }, { "parameters": { - "jsCode": "const question = $('Webhook').first().json.body?.question || '';\nreturn [{ json: { type:'chat', question, ollamaBody: { model: 'qwen2.5:3b', stream: false, options: { temperature: 0.2, top_p: 0.8, num_predict: 512 }, messages: [ { role: 'system', content: `Sen Türkçe konuşan yardımcı bir asistansın. Kullanıcı SQL/veri istemiyorsa kısa ve net cevap ver. Veritabanı sorgusu üretme.` }, { role: 'user', content: question } ] } } }];" + "jsCode": "\nconst body = $('Webhook').first().json.body || {};\nconst question = String(body.question || body.message || '').trim();\nreturn [{ json: { type:'chat', question, ollamaBody: {\n model: body.chatModel || 'qwen2.5:3b',\n stream: false,\n options: { temperature: 0.2, top_p: 0.8, num_predict: 512 },\n messages: [\n { role: 'system', content: 'Sen Türkçe konuşan yardımcı bir asistansın. Kullanıcı SQL/veri istemiyorsa kısa ve net cevap ver. Veritabanı sorgusu üretme.' },\n { role: 'user', content: question }\n ]\n}}}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 180, - 420 + -31824, + -2944 ], - "id": "41617000-9033-4896-b04a-d2dfb48567ba", + "id": "6528b16c-51e1-4d19-9806-6d2f790ae6c2", "name": "Build Chat Request" }, { @@ -123,37 +124,37 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 390, - 420 + -31600, + -2944 ], - "id": "b2f42e30-af31-447a-8e17-dc2e01bf2d96", + "id": "c5334fb9-eded-4903-bc2f-6454fd800e2a", "name": "Chat Ollama" }, { "parameters": { - "jsCode": "const res = $input.first().json || {};\nreturn [{ json: { type: 'chat', question: $('Webhook').first().json.body?.question || '', answer: res.message?.content || res.response || res.output || '' } }];" + "jsCode": "const res=$input.first().json||{}; return [{json:{type:'chat', question:$('Webhook').first().json.body?.question || '', answer:res.message?.content || res.response || res.output || ''}}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 610, - 420 + -31376, + -2944 ], - "id": "c9fae17b-b4d7-48df-8556-47ccc56556fb", + "id": "468e3ad7-1c0e-45db-bfd0-63b391247393", "name": "Chat Response" }, { "parameters": { - "jsCode": "const dictionaryQuery = `\nSELECT\n STRING_AGG(\n CAST(\n N'Varlık: ' + ISNULL(EntityName, N'')\n + N' | Tablo: ' + ISNULL(TableName, N'')\n + N' | Kolon: ' + ISNULL(ColumnName, N'')\n + N' | Tip: ' + ISNULL(DataType, N'')\n + N' | Açıklama: ' + ISNULL(ColumnDescription, N'')\n + N' | Eş Anlamlılar: ' + ISNULL(Synonyms, N'')\n + CASE WHEN IsDefaultSelectColumn = 1 THEN N' | Varsayılan gösterilecek kolon' ELSE N'' END\n + CASE WHEN IsActiveColumn = 1 THEN N' | Aktiflik kolonu' ELSE N'' END\n + CASE WHEN IsDeletedColumn = 1 THEN N' | Silinmiş kayıt kolonu' ELSE N'' END\n + CASE WHEN IsSensitive = 1 THEN N' | Hassas alan' ELSE N'' END\n AS NVARCHAR(MAX)),\n CHAR(10)\n ) AS DataDictionaryPrompt\nFROM dbo.AI_DataDictionary;\n`;\nreturn [{ json: { question: $json.question, tenantName: $json.tenantName || 'Sozsoft', dictionaryQuery } }];" + "jsCode": "const body = $('Webhook').first().json.body || {};\n\nconst question = String(body.question || body.message || '').trim();\nconst tenantName = String(body.tenantName || 'Sozsoft').trim();\nconst definitionCultureName = body.definitionCultureName || 'en';\nconst uiCultureName = body.uiCultureName || 'tr';\nconst listFormCode = body.listFormCode || null;\nconst onlyBaseDefinitions = body.onlyBaseDefinitions === false ? 0 : 1;\nconst resourceName = body.resourceName || null;\nconst maxForms = Number(body.maxForms || 500);\n\nfunction q(v) {\n if (v === null || v === undefined || v === '') return 'NULL';\n return \"N'\" + String(v).replace(/'/g, \"''\") + \"'\";\n}\n\nconst safeMaxForms = Number.isFinite(maxForms) && maxForms > 0 ? Math.min(maxForms, 2000) : 500;\n\n// v23: Sadece dbo.Sas_H_DbTableColumn tablosundan okur.\nconst dictionaryQuery = `\nSELECT TOP (1)\n JsonData AS ListFormDataDictionaryJson,\n CacheKey,\n RefreshedAt,\n FormCount,\n FieldCount,\n JsonLength\nFROM dbo.Sas_H_DbTableColumn WITH (NOLOCK)\nWHERE DictionaryType = N'ListFormJson'\n AND ISNULL(IsActive, 0) = 1\n AND DefinitionCultureName = ${q(definitionCultureName)}\n AND UiCultureName = ${q(uiCultureName)}\n AND ISNULL(ListFormCode, N'') = ISNULL(${q(listFormCode)}, N'')\n AND ISNULL(OnlyBaseDefinitions, 1) = ${onlyBaseDefinitions}\n AND ISNULL(ResourceName, N'') = ISNULL(${q(resourceName)}, N'')\n AND ISNULL(MaxForms, 500) = ${safeMaxForms}\nORDER BY RefreshedAt DESC;\n`;\n\nreturn [{\n json: {\n question,\n tenantName,\n definitionCultureName,\n uiCultureName,\n listFormCode,\n onlyBaseDefinitions,\n resourceName,\n maxForms: safeMaxForms,\n dictionaryQuery\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 180, - 40 + -31824, + -3360 ], - "id": "f92e673a-50ac-4551-8cff-a583f39e7ec3", - "name": "Build Data Dictionary Query" + "id": "d96b157a-de5e-4cc3-ba7d-ef2030ca7755", + "name": "Build Dictionary Query" }, { "parameters": { @@ -163,23 +164,29 @@ "type": "n8n-nodes-base.microsoftSql", "typeVersion": 1.1, "position": [ - 420, - 40 + -31584, + -3360 ], - "id": "61640dc9-12d9-4282-9277-b4760ac8f0e9", - "name": "Get Data Dictionary Prompt" + "id": "70d35887-291c-41e5-acac-e5c5c60e0196", + "name": "Get Dictionary JSON", + "credentials": { + "microsoftSql": { + "id": "FV4335uaFWUGPlIf", + "name": "Microsoft SQL account" + } + } }, { "parameters": { - "jsCode": "const dict = String($input.first().json.DataDictionaryPrompt || '').trim();\nconst question = $('Webhook').first().json.body?.question || '';\nconst tenantName = $('Webhook').first().json.body?.tenantName || 'Sozsoft';\n\nconst tableNames = [...new Set([...dict.matchAll(/\\|\\s*Tablo:\\s*([^|\\n]+)/g)].map(m => m[1].trim()).filter(Boolean))];\nconst activeColumns = [...new Set([...dict.matchAll(/\\|\\s*Tablo:\\s*([^|\\n]+)\\s*\\|\\s*Kolon:\\s*([^|\\n]+)[^\\n]*Aktiflik kolonu/g)].map(m => `${m[1].trim()}.${m[2].trim()}`))];\nconst deletedColumns = [...new Set([...dict.matchAll(/\\|\\s*Tablo:\\s*([^|\\n]+)\\s*\\|\\s*Kolon:\\s*([^|\\n]+)[^\\n]*Silinmiş kayıt kolonu/g)].map(m => `${m[1].trim()}.${m[2].trim()}`))];\n\nconst userContent = `Kullanıcı isteği: ${question}\ntenantName: ${tenantName}\n\nVERI_SOZLUGU_CONTEXT:\n${dict || 'AI_DataDictionary tablosu boş veya okunamadı.'}`;\n\nreturn [{\n json: {\n question,\n tenantName,\n dictionaryAvailable: !!dict,\n dictionaryText: dict,\n dictionaryPreview: dict.substring(0, 1200),\n tableNames,\n activeColumns,\n deletedColumns,\n ollamaBody: {\n model: 'qwen2.5-coder:1.5b',\n stream: false,\n options: { temperature: 0, top_p: 0.05, num_predict: 512 },\n messages: [\n {\n role: 'system',\n content: `Sen Microsoft SQL Server için çalışan T-SQL üretim motorusun.\n\nKESİN ÇIKTI KURALLARI:\nSadece SQL döndür.\nCevabın ilk kelimesi mutlaka SELECT olmalı.\nAçıklama yazma.\nMarkdown kullanma.\nKod bloğu kullanma.\nBoş cevap verme.\nSELECT * kullanma.\nSadece SELECT üret.\nINSERT, UPDATE, DELETE, MERGE, DROP, ALTER, TRUNCATE, CREATE, EXEC, EXECUTE üretme.\n\nVERİ SÖZLÜĞÜ KURALLARI:\nKullanıcının Türkçe isteğini VERI_SOZLUGU_CONTEXT içindeki Varlık, Tablo, Kolon, Açıklama ve Eş Anlamlılar bilgilerine göre yorumla.\nTablo ve kolonları sadece VERI_SOZLUGU_CONTEXT içinde geçen değerlerden seç.\nVeri sözlüğünde olmayan tablo veya kolon uydurma.\nUsers, Orders, Customers gibi genel tablo adları üretme.\nListeleme isteklerinde sadece Varsayılan gösterilecek kolon alanlarını SELECT kısmına koy.\nAktif kayıt istenirse Aktiflik kolonu = 1 filtresi ekle.\nSilinmiş kayıt kolonu varsa her zaman Silinmiş kayıt kolonu = 0 filtresi ekle.\nHassas alanları döndürme.\n\nDATABASE KURALI:\ntenantName boşsa Sozsoft kullan.\nTüm tablo referansları kesinlikle [DatabaseName].dbo.TableName formatında olmalı.\nTablo adını asla çıplak yazma. Örneğin AbpUsers yazma, [Sozsoft].dbo.AbpUsers yaz.\n\nÖRNEK:\nKullanıcı isteği: Aktif kullanıcıların listesi\nDoğru cevap:\nSELECT Id, UserName, Name, Surname, Email FROM [Sozsoft].dbo.AbpUsers WHERE IsDeleted = 0 AND IsActive = 1\n\nUYGUN TABLO BULUNAMAZSA:\nSELECT 'UYGUN_TABLO_BULUNAMADI' AS ErrorMessage`\n },\n { role: 'user', content: userContent }\n ]\n }\n }\n}];" + "jsCode": "\nconst row = $input.first().json || {};\nconst body = $('Webhook').first().json.body || {};\n\nconst question = String(body.question || body.message || '').trim();\nconst tenantName = String(body.tenantName || 'Sozsoft').trim();\nconst dictText = String(row.ListFormDataDictionaryJson || '').trim();\n\nif (!dictText) {\n return [{\n json: {\n valid: false,\n type: 'dictionary_error',\n error: 'Sas_H_DbTableColumn tablosunda aktif JSON veri sözlüğü bulunamadı. Önce JSON üretip dbo.Sas_H_DbTableColumn tablosuna yazın.',\n question\n }\n }];\n}\n\nlet dict;\ntry {\n dict = JSON.parse(dictText);\n} catch (e) {\n return [{\n json: {\n valid: false,\n type: 'dictionary_error',\n error: 'JsonData parse edilemedi: ' + e.message,\n question\n }\n }];\n}\n\nconst listForms = Array.isArray(dict.listForms) ? dict.listForms : [];\n\nfunction escapeRegExp(s) {\n return String(s || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeText(value) {\n return String(value || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/"/gi, '')\n .replace(/[\\\"'`[\\]\\\\]/g, ' ')\n .toLocaleLowerCase('tr-TR')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/ı/g, 'i')\n .replace(/ğ/g, 'g')\n .replace(/ü/g, 'u')\n .replace(/ş/g, 's')\n .replace(/ö/g, 'o')\n .replace(/ç/g, 'c')\n .replace(/[^a-z0-9]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction decodeSqlEscapes(value) {\n return String(value || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/u0022/g, '')\n .replace(/\\\\u0027/g, \"'\")\n .replace(/\\u0027/g, \"'\")\n .replace(/u0027/g, \"'\")\n .replace(/"/gi, '')\n .replace(/'/gi, \"'\");\n}\n\nfunction cleanIdentifier(value) {\n let s = decodeSqlEscapes(value)\n .replace(/LookupQuery\\s*:/gi, '')\n .replace(/[\"'`\\[\\]\\\\]/g, '')\n .replace(/\\s+AS\\s+(KEY|NAME)$/i, '')\n .replace(/^dbo\\./i, '')\n .trim();\n\n const dbRe = new RegExp('^' + escapeRegExp(tenantName) + '\\\\.', 'i');\n s = s.replace(dbRe, '').replace(/^dbo\\./i, '').trim();\n\n const parts = s.split('.').map(x => x.trim()).filter(Boolean);\n s = parts.length ? parts[parts.length - 1] : s;\n\n // Only allow real identifier tokens. SQL expressions such as CONCAT(Name,' (',DisplayName,')') are not column names.\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) return '';\n return s;\n}\n\nfunction simpleIdentifierFromExpression(value) {\n let s = decodeSqlEscapes(value).trim();\n s = s.replace(/^\\s*SELECT\\s+/i, '').replace(/\\s+AS\\s+(KEY|NAME)\\s*$/i, '').trim();\n\n // remove outer brackets/quotes/backticks for a simple identifier only\n if (/^\\[[A-Za-z_][A-Za-z0-9_]*\\]$/.test(s)) s = s.slice(1, -1);\n if (/^\"[A-Za-z_][A-Za-z0-9_]*\"$/.test(s)) s = s.slice(1, -1);\n if (/^`[A-Za-z_][A-Za-z0-9_]*`$/.test(s)) s = s.slice(1, -1);\n\n // dbo.Column or Table.Column => use last part only if all parts are simple identifiers\n const parts = s.split('.').map(x => x.replace(/[\\[\\]\"`]/g, '').trim()).filter(Boolean);\n if (parts.length && parts.every(x => /^[A-Za-z_][A-Za-z0-9_]*$/.test(x))) return parts[parts.length - 1];\n\n return '';\n}\n\nfunction bracketIdentifier(value, fallback = 'Id') {\n const id = cleanIdentifier(value) || cleanIdentifier(fallback) || 'Id';\n return '[' + id.replace(/]/g, ']]') + ']';\n}\n\nfunction cleanAliasLabel(value, fallback) {\n let label = decodeSqlEscapes(value)\n .replace(/\\r|\\n|\\t/g, ' ')\n .replace(/[\\[\\]]/g, '')\n .replace(/[\\u0000-\\u001F]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n // Alias kullanıcıya görünen başlıktır; Türkçe karakter ve boşluk kalabilir.\n // Ancak SQL ifadesi veya boş değer alias olamaz.\n if (!label || /^(CONCAT|CASE|SELECT|NULLIF|ISNULL|COALESCE)\\s*\\(/i.test(label)) {\n label = decodeSqlEscapes(fallback).replace(/[\\[\\]]/g, '').trim();\n }\n if (!label) label = 'Column';\n return label.slice(0, 128);\n}\n\nfunction bracketAlias(value, fallback = 'Column') {\n const label = cleanAliasLabel(value, fallback);\n return '[' + label.replace(/]/g, ']]') + ']';\n}\n\nfunction splitTokens(text) {\n const n = normalizeText(text);\n if (!n) return [];\n return n.split(/\\s+/).filter(Boolean);\n}\n\nfunction uniqueTokens(text) {\n return [...new Set(splitTokens(text))];\n}\n\nfunction parseLookupSql(text) {\n const s = decodeSqlEscapes(text)\n .replace(/[\"`\\[\\]\\\\]/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n\n // Generic SQL lookup pattern:\n // SELECT AS Key, AS Name FROM ...\n const m = s.match(/\\bselect\\s+(.+?)\\s+as\\s+key\\s*,\\s*(.+?)\\s+as\\s+name\\s+from\\s+([a-zA-Z0-9_.]+)/i);\n if (!m) return null;\n\n const valueColumn = simpleIdentifierFromExpression(m[1]);\n let displayColumn = simpleIdentifierFromExpression(m[2]);\n\n // If display expression is a SQL expression like CONCAT(...), do not treat it as a column.\n // Use the lookup key column as a safe display fallback instead of generating [Lx].[CONCAT(...)] or [L0].\n if (!displayColumn) displayColumn = valueColumn;\n\n return {\n tableName: cleanIdentifier(m[3]),\n valueColumn,\n displayColumn\n };\n}\n\nfunction compactField(field) {\n const parsedLookup = parseLookupSql(field.lookupJsonText || field.lookupJson || '');\n const sqlColumnName = cleanIdentifier(field.sqlColumnName || field.field);\n const displayName = String(field.displayName || field.captionValue || field.captionKey || sqlColumnName || '');\n\n const lookupTableName = cleanIdentifier(\n parsedLookup?.tableName ||\n field.lookupTableName ||\n field.parsedLookupTableName ||\n ''\n );\n\n const lookupValueColumn = cleanIdentifier(\n parsedLookup?.valueColumn ||\n field.parsedLookupValueColumn ||\n field.lookupValueColumn ||\n ''\n );\n\n let lookupDisplayColumn = cleanIdentifier(\n parsedLookup?.displayColumn ||\n field.parsedLookupDisplayColumn ||\n field.lookupDisplayColumn ||\n ''\n );\n\n if (!lookupDisplayColumn && lookupValueColumn) lookupDisplayColumn = lookupValueColumn;\n\n // A field is a usable lookup only when table/value/display are clean real identifiers.\n // This prevents SQL like [L0].[u0027)u0027)] and [L1].[CONCAT(...)] from being generated.\n const hasLookup = Boolean(lookupTableName && lookupValueColumn && lookupDisplayColumn);\n let lookupJoinMode = hasLookup ? (field.lookupJoinMode || '') : '';\n\n if (hasLookup && !lookupJoinMode) {\n const baseNorm = normalizeText(sqlColumnName);\n const valueNorm = normalizeText(lookupValueColumn);\n const idLike = baseNorm.endsWith('id') || valueNorm === 'id';\n lookupJoinMode = idLike ? 'convert-nvarchar-safe' : 'direct-string';\n }\n\n return {\n sqlColumnName,\n displayName,\n captionKey: String(field.captionKey || ''),\n sqlDataType: String(field.sqlDataType || ''),\n listOrderNo: Number(field.listOrderNo ?? 999999),\n isBooleanColumn: Boolean(field.isBooleanColumn),\n isVisible: field.isVisible !== false,\n isDefaultSelectColumn: Boolean(field.isDefaultSelectColumn),\n isActiveStatusColumn: Boolean(field.isActiveStatusColumn),\n isDeletedStatusColumn: Boolean(field.isDeletedStatusColumn),\n isSensitive: Boolean(field.isSensitive) || /password|token|secret|apikey|privatekey|connectionstring/i.test(sqlColumnName),\n hasLookup,\n lookup: hasLookup ? {\n tableName: lookupTableName,\n valueColumn: lookupValueColumn,\n displayColumn: lookupDisplayColumn || lookupValueColumn,\n joinMode: lookupJoinMode,\n fallbackToSourceColumn: Boolean(field.lookupFallbackToSourceColumn)\n } : null,\n searchText: normalizeText([\n field.sqlColumnName,\n field.field,\n field.displayName,\n field.captionValue,\n field.captionKey,\n field.lookupSearchText\n ].join(' '))\n };\n}\n\nfunction compactForm(form) {\n const fields = Array.isArray(form.fields) ? form.fields.map(compactField).filter(f => f.sqlColumnName && !f.isSensitive) : [];\n return {\n formCode: String(form.formCode || form.listFormCode || ''),\n displayName: String(form.displayName || form.nameValue || form.title || ''),\n tableName: cleanIdentifier(form.sqlTableName || form.tableName || form.sqlSourceName || ''),\n primaryKeyColumn: cleanIdentifier(form.primaryKeyColumn || form.keyFieldName || 'Id'),\n sourceKind: form.sourceKind || 'table',\n searchText: normalizeText([\n form.formCode,\n form.listFormCode,\n form.displayName,\n form.nameValue,\n form.title,\n form.description,\n form.tableName,\n form.sqlTableName,\n form.searchText\n ].join(' ')),\n fields\n };\n}\n\nconst compactAllForms = listForms.map(compactForm).filter(f => f.tableName && f.fields.length > 0);\n\n// Generic question semantics; these are filter/action words, not entity names.\n// They are intentionally not mapped to any specific table.\nconst actionOrFilterWords = new Set([\n 'liste', 'listesi', 'listele', 'goster', 'getir', 'ver', 'bul', 'ara',\n 'aktif', 'pasif', 'active', 'inactive', 'etkin', 'silinmis', 'silinen',\n 'toplam', 'say', 'adet', 'kac', 'rapor'\n]);\n\nconst questionTokens = uniqueTokens(question);\nconst discriminatingTokens = questionTokens.filter(t => !actionOrFilterWords.has(t));\n\nfunction tokenHitScore(token, text) {\n if (!token || !text) return 0;\n if (text.split(' ').includes(token)) return 12;\n if (token.length >= 3 && text.includes(token)) return 6;\n return 0;\n}\n\nfunction scoreForm(form) {\n const formText = normalizeText([form.formCode, form.displayName, form.tableName, form.searchText].join(' '));\n const fieldText = normalizeText(form.fields.map(f => [f.sqlColumnName, f.displayName, f.captionKey].join(' ')).join(' '));\n\n let score = 0;\n\n for (const token of discriminatingTokens) {\n score += tokenHitScore(token, formText) * 4;\n score += tokenHitScore(token, fieldText);\n }\n\n // If no discriminating token exists, allow generic tokens to select by visible labels.\n if (discriminatingTokens.length === 0) {\n for (const token of questionTokens) {\n score += tokenHitScore(token, formText);\n score += tokenHitScore(token, fieldText) / 2;\n }\n }\n\n // Exact normalized phrase in form/table/display name gets a generic boost.\n const qNorm = normalizeText(question);\n if (qNorm && formText.includes(qNorm)) score += 30;\n\n // Penalize forms that only match action/filter words.\n if (score === 0 && questionTokens.some(t => actionOrFilterWords.has(t)) && form.fields.some(f => f.isActiveStatusColumn)) {\n score += 1;\n }\n\n return score;\n}\n\nconst maxCandidateForms = Math.max(1, Math.min(Number(body.maxCandidateForms || 8), 25));\nconst scored = compactAllForms\n .map(form => ({ form, score: scoreForm(form) }))\n .sort((a, b) => b.score - a.score || a.form.formCode.localeCompare(b.form.formCode));\n\nlet compactForms = scored.filter(x => x.score > 0).slice(0, maxCandidateForms).map(x => x.form);\nif (compactForms.length === 0) compactForms = compactAllForms.slice(0, maxCandidateForms);\n\nconst selectedForm = compactForms[0] || null;\n\nconst allowedTables = new Set();\nfor (const form of compactForms) {\n if (form.tableName) allowedTables.add(form.tableName);\n for (const f of form.fields || []) {\n if (f.lookup?.tableName) allowedTables.add(f.lookup.tableName);\n }\n}\n\nfunction aliasFor(index) {\n return '[L' + index + ']';\n}\n\nfunction sqlExpressionForField(baseAlias, lookupAlias, field) {\n const col = bracketIdentifier(field.sqlColumnName);\n const alias = bracketAlias(field.displayName, field.sqlColumnName);\n\n if (lookupAlias && lookupAlias !== '[L0]' && field.hasLookup && field.lookup?.tableName && field.lookup?.displayColumn) {\n const displayColumn = bracketIdentifier(field.lookup.displayColumn);\n if (field.lookup.fallbackToSourceColumn) {\n return `COALESCE(CONVERT(NVARCHAR(4000), ${lookupAlias}.${displayColumn}), CONVERT(NVARCHAR(4000), ${baseAlias}.${col})) AS ${alias}`;\n }\n return `${lookupAlias}.${displayColumn} AS ${alias}`;\n }\n\n if (field.isBooleanColumn) {\n return `CASE WHEN ${baseAlias}.${col} = 1 THEN 'Yes' WHEN ${baseAlias}.${col} = 0 THEN 'No' ELSE NULL END AS ${alias}`;\n }\n\n return `${baseAlias}.${col} AS ${alias}`;\n}\n\nfunction buildDirectSql(form, questionText) {\n if (!form || !form.tableName) return '';\n\n const baseAlias = '[T]';\n const qNorm = normalizeText(questionText);\n const fields = (form.fields || [])\n .filter(f => f.sqlColumnName && !f.isSensitive && f.isVisible !== false)\n .sort((a, b) => a.listOrderNo - b.listOrderNo);\n\n let selectedFields = fields.filter(f => f.isDefaultSelectColumn);\n if (selectedFields.length === 0) selectedFields = fields.slice(0, 12);\n\n selectedFields = selectedFields\n .filter(f => !/^id$/i.test(f.sqlColumnName))\n .slice(0, Math.max(1, Math.min(Number(body.maxSelectColumns || 14), 30)));\n\n const joins = [];\n const lookupAliasByColumn = new Map();\n let joinIndex = 1;\n\n for (const field of selectedFields) {\n if (!field.hasLookup || !field.lookup?.tableName || !field.lookup?.valueColumn || !field.lookup?.displayColumn) continue;\n\n const lookupAlias = aliasFor(joinIndex++);\n lookupAliasByColumn.set(field.sqlColumnName, lookupAlias);\n\n const baseColumn = bracketIdentifier(field.sqlColumnName);\n const valueColumn = bracketIdentifier(field.lookup.valueColumn);\n const lookupTable = bracketIdentifier(field.lookup.tableName);\n\n const joinCondition = field.lookup.joinMode === 'direct-string'\n ? `${baseAlias}.${baseColumn} = ${lookupAlias}.${valueColumn}`\n : `CONVERT(NVARCHAR(100), ${baseAlias}.${baseColumn}) = CONVERT(NVARCHAR(100), ${lookupAlias}.${valueColumn})`;\n\n joins.push(`LEFT JOIN [${tenantName}].[dbo].${lookupTable} AS ${lookupAlias} ON ${joinCondition}`);\n }\n\n const selectParts = selectedFields.map(field =>\n sqlExpressionForField(baseAlias, lookupAliasByColumn.get(field.sqlColumnName) || '[L0]', field)\n );\n\n if (selectParts.length === 0) {\n selectParts.push(`${baseAlias}.${bracketIdentifier(form.primaryKeyColumn || 'Id')} AS [Id]`);\n }\n\n const where = [];\n const activeField = fields.find(f => f.isActiveStatusColumn) || fields.find(f => normalizeText(f.sqlColumnName) === 'isactive');\n const deletedField = fields.find(f => f.isDeletedStatusColumn) || fields.find(f => normalizeText(f.sqlColumnName) === 'isdeleted');\n\n const asksActive = qNorm.split(' ').includes('aktif') || qNorm.split(' ').includes('active') || qNorm.split(' ').includes('etkin');\n const asksPassive = qNorm.split(' ').includes('pasif') || qNorm.split(' ').includes('inactive');\n\n if (activeField && asksActive) where.push(`${baseAlias}.${bracketIdentifier(activeField.sqlColumnName)} = 1`);\n if (activeField && asksPassive) where.push(`${baseAlias}.${bracketIdentifier(activeField.sqlColumnName)} = 0`);\n if (deletedField) where.push(`${baseAlias}.${bracketIdentifier(deletedField.sqlColumnName)} = 0`);\n\n return `SELECT ${selectParts.join(', ')} FROM [${tenantName}].[dbo].${bracketIdentifier(form.tableName)} AS ${baseAlias}` +\n (joins.length ? ' ' + joins.join(' ') : '') +\n (where.length ? ' WHERE ' + where.join(' AND ') : '');\n}\n\nconst directSql = buildDirectSql(selectedForm, question);\n\nconst systemPrompt = `Sen Microsoft SQL Server T-SQL üretim motorusun.\nSadece SELECT sorgusu döndür. Açıklama, özet, yorum, JSON açıklaması, markdown veya kod bloğu yazma.\nİlk kelime kesinlikle SELECT olmalı.\nSadece verilen JSON veri sözlüğündeki form, tablo, kolon ve lookup bilgilerini kullan.\nTablo referansları [${tenantName}].[dbo].[TableName] formatında olmalı.\nSELECT * kullanma.\nINSERT, UPDATE, DELETE, MERGE, DROP, ALTER, TRUNCATE, CREATE, EXEC, EXECUTE üretme.\nLookup alanlarda kullanıcı ID istemedikçe lookup.displayColumn değerini LEFT JOIN ile göster.\nlookup.valueColumn değerini kolon adı olarak aynen kullan. \"LookupQuery:\" gibi metinler kolon adı değildir.\nlookup.joinMode direct-string ise ON [T].[baseColumn] = [Lx].[lookup.valueColumn] kullan.\nlookup.joinMode convert-nvarchar-safe ise ON CONVERT(NVARCHAR(100), [T].[baseColumn]) = CONVERT(NVARCHAR(100), [Lx].[lookup.valueColumn]) kullan.\nBoolean alanları SELECT çıktısında CASE ile Yes/No olarak göster.\nAktif/pasif filtreleri yalnızca veri sözlüğünde uygun aktiflik kolonu varsa uygulanır.\nHassas alanları döndürme.`;\n\nconst userContent = `Kullanıcı isteği: ${question}\n\nADAY_VERI_SOZLUGU_JSON:\n${JSON.stringify({ listForms: compactForms }, null, 2)}\n\nDETERMINISTIK_SQL:\n${directSql}\n\nDeterministik SQL kullanıcı isteğini karşılıyorsa aynen döndür.`;\n\nreturn [{\n json: {\n valid: true,\n question,\n tenantName,\n dictionaryMeta: {\n cacheKey: row.CacheKey,\n refreshedAt: row.RefreshedAt,\n formCount: row.FormCount,\n fieldCount: row.FieldCount,\n jsonLength: row.JsonLength\n },\n candidateFormCodes: compactForms.map(f => f.formCode),\n formScores: scored.slice(0, maxCandidateForms).map(x => ({ formCode: x.form.formCode, tableName: x.form.tableName, score: x.score })),\n selectedFormCode: selectedForm?.formCode || null,\n selectedTableName: selectedForm?.tableName || null,\n directSql,\n compactDictionary: { listForms: compactForms },\n allowedTables: [...allowedTables],\n ollamaBody: {\n model: body.sqlModel || 'qwen2.5-coder:1.5b',\n stream: false,\n options: {\n temperature: 0,\n top_p: 0.05,\n num_predict: Math.max(80, Math.min(Number(body.numPredict || 220), 500)),\n stop: [';\\n\\n', 'Açıklama:', 'Explanation:', 'Not:', '```']\n },\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userContent }\n ]\n }\n }\n}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 660, - 40 + -31344, + -3360 ], - "id": "734269a4-9b8d-44ea-a879-18cb919e75e9", + "id": "7b44191c-172d-464b-ba11-bdd5728d25fc", "name": "Build SQL Request" }, { @@ -194,23 +201,23 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 900, - 40 + -31104, + -3360 ], - "id": "a8b8e100-1a8d-4fb7-8678-86eac042b4b1", + "id": "4ca40c6f-3605-4e7c-8beb-2d25784094d8", "name": "SQL Ollama" }, { "parameters": { - "jsCode": "const res = $input.first().json || {};\nlet raw = res.message?.content || res.response || res.output || res.text || res.answer || '';\nraw = String(raw || '').trim();\n\nconst question = $('Webhook').first().json.body?.question || '';\nconst tenantName = $('Webhook').first().json.body?.tenantName || 'Sozsoft';\nconst buildNode = $('Build SQL Request').first().json || {};\nconst tableNames = buildNode.tableNames || [];\nconst deletedColumns = buildNode.deletedColumns || [];\n\nfunction cleanGeneratedSql(text) {\n let t = String(text || '')\n .replace(/```sql/gi, '')\n .replace(/```/g, '')\n .replace(/^SQL\\s*:/i, '')\n .trim();\n\n const selectIndex = t.search(/\\bSELECT\\b/i);\n if (selectIndex < 0) return '';\n t = t.substring(selectIndex).trim();\n\n const secondSelectMatch = t.substring(6).search(/\\bSELECT\\b/i);\n if (secondSelectMatch >= 0) t = t.substring(0, 6 + secondSelectMatch).trim();\n\n const semicolonIndex = t.indexOf(';');\n if (semicolonIndex >= 0) t = t.substring(0, semicolonIndex).trim();\n\n const cutPatterns = [/\\n\\s*Açıklama\\s*:/i, /\\n\\s*Explanation\\s*:/i, /\\n\\s*Bu sorgu/i, /\\n\\s*Not\\s*:/i];\n for (const p of cutPatterns) {\n const m = t.search(p);\n if (m > 0) t = t.substring(0, m).trim();\n }\n return t.trim();\n}\n\nfunction escapeRegExp(s) {\n return String(s).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction qualifyTables(sql) {\n let q = sql;\n const db = tenantName || 'Sozsoft';\n const uniqueTables = [...new Set(tableNames)].sort((a, b) => b.length - a.length);\n for (const table of uniqueTables) {\n const t = escapeRegExp(table);\n q = q.replace(new RegExp(`\\\\b(FROM|JOIN)\\\\s+(?!\\\\[)(?:dbo\\\\.)?(${t})\\\\b`, 'gi'), `$1 [${db}].dbo.$2`);\n }\n return q;\n}\n\nfunction addDefaultIsDeletedFilter(sql) {\n let q = sql.trim();\n for (const item of deletedColumns) {\n const [table, col] = String(item).split('.');\n if (!table || !col) continue;\n\n const tableRegex = new RegExp(`(\\\\[${escapeRegExp(tenantName)}\\\\]\\\\.dbo\\\\.|dbo\\\\.|\\\\b)${escapeRegExp(table)}\\\\b`, 'i');\n if (!tableRegex.test(q)) continue;\n\n const whereMatch = q.match(/\\bWHERE\\b([\\s\\S]*?)(\\bGROUP\\s+BY\\b|\\bORDER\\s+BY\\b|$)/i);\n const whereText = whereMatch ? whereMatch[1] : '';\n const colInWhere = new RegExp(`\\\\b${escapeRegExp(col)}\\\\b`, 'i').test(whereText);\n if (colInWhere) continue;\n\n const condition = `${col} = 0`;\n const orderGroupMatch = q.match(/\\b(GROUP\\s+BY|ORDER\\s+BY)\\b/i);\n if (/\\bWHERE\\b/i.test(q)) {\n if (orderGroupMatch) {\n const idx = orderGroupMatch.index;\n q = q.slice(0, idx).trim() + ` AND ${condition} ` + q.slice(idx).trim();\n } else {\n q = q + ` AND ${condition}`;\n }\n } else {\n if (orderGroupMatch) {\n const idx = orderGroupMatch.index;\n q = q.slice(0, idx).trim() + ` WHERE ${condition} ` + q.slice(idx).trim();\n } else {\n q = q + ` WHERE ${condition}`;\n }\n }\n }\n return q;\n}\n\nlet sql = cleanGeneratedSql(raw);\nsql = qualifyTables(sql);\nsql = addDefaultIsDeletedFilter(sql);\nsql = sql.replace(/;\\s*$/,'').trim();\n\nif (!sql) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Üretilen çıktı içinde SELECT bulunamadı.', question, generatedText: raw, ollamaRawOutput: res, dictionaryAvailable: buildNode.dictionaryAvailable, dictionaryPreview: buildNode.dictionaryPreview } }];\n}\n\nif (!/^\\s*SELECT\\b/i.test(sql)) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Üretilen çıktı SELECT ile başlamıyor.', question, generatedText: raw, cleanedText: sql, ollamaRawOutput: res, dictionaryAvailable: buildNode.dictionaryAvailable, dictionaryPreview: buildNode.dictionaryPreview } }];\n}\n\nconst forbidden = /\\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|TRUNCATE|CREATE|EXEC|EXECUTE|GRANT|REVOKE|BACKUP|RESTORE)\\b/i;\nif (forbidden.test(sql)) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Güvenlik nedeniyle sadece SELECT sorgularına izin veriliyor.', question, generatedText: raw, cleanedText: sql } }];\n}\n\nconst withoutFirstSelect = sql.replace(/^\\s*SELECT\\b/i, '');\nif (/\\bSELECT\\b/i.test(withoutFirstSelect)) {\n return [{ json: { valid: false, type: 'sql_error', error: 'Tek seferde sadece bir SELECT sorgusu çalıştırılabilir.', question, generatedText: raw, cleanedText: sql } }];\n}\n\nreturn [{ json: { valid: true, type: 'sql', question, tenantName, sql, generatedText: raw } }];" + "jsCode": "const res = $input.first().json || {};\nconst build = $('Build SQL Request').first().json || {};\nconst question = build.question || $('Webhook').first().json.body?.question || '';\nconst tenantName = String(build.tenantName || $('Webhook').first().json.body?.tenantName || 'Sozsoft').trim();\n\nfunction escapeRegExp(s) {\n return String(s || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction cleanName(v) {\n let s = String(v || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/"/gi, '')\n .replace(/LookupQuery\\s*:/gi, '')\n .replace(/[\"`\\[\\]\\\\]/g, '')\n .replace(/^dbo\\./i, '')\n .trim();\n\n const dbRe = new RegExp('^' + escapeRegExp(tenantName) + '\\\\.', 'i');\n s = s.replace(dbRe, '').replace(/^dbo\\./i, '').trim();\n\n const parts = s.split('.').map(x => x.trim()).filter(Boolean);\n return parts.length ? parts[parts.length - 1] : s;\n}\n\nconst allowedTables = new Set((build.allowedTables || []).map(cleanName).filter(Boolean));\n\nfunction getOllamaText(obj) {\n return String(\n obj.message?.content ||\n obj.response ||\n obj.output ||\n obj.text ||\n obj.content ||\n ''\n ).trim();\n}\n\nconst raw = getOllamaText(res);\n\nfunction cleanSql(text) {\n let t = String(text || '')\n .replace(/```sql/gi, '')\n .replace(/```/g, '')\n .replace(/^SQL\\s*:/i, '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/"/gi, '')\n .replace(/\"/g, '')\n .trim();\n\n const idx = t.search(/\\bSELECT\\b/i);\n if (idx < 0) return '';\n\n t = t.substring(idx).trim();\n const semi = t.indexOf(';');\n if (semi >= 0) t = t.substring(0, semi).trim();\n return t.replace(/\\s+/g, ' ').trim();\n}\n\nfunction canonicalizeSql(sql) {\n let q = String(sql || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/"/gi, '')\n .replace(/\\[LookupQuery:\\s*([^\\]]+)\\]/gi, '[$1]');\n\n const db = escapeRegExp(tenantName);\n\n // [Sozsoft].[Sozsoft].[dbo].[X] -> [Sozsoft].[dbo].[X]\n q = q.replace(new RegExp('\\\\[' + db + '\\\\]\\\\.\\\\[' + db + '\\\\]\\\\.\\\\[dbo\\\\]\\\\.', 'gi'), '[' + tenantName + '].[dbo].');\n q = q.replace(new RegExp(db + '\\\\.' + db + '\\\\.dbo\\\\.', 'gi'), '[' + tenantName + '].[dbo].');\n\n // [dbo].[X] -> [Sozsoft].[dbo].[X], eğer önünde zaten [Sozsoft]. yoksa\n q = q.replace(/\\[dbo\\]\\.\\[([^\\]]+)\\]/gi, (m, t, off, str) => {\n const before = str.slice(Math.max(0, off - tenantName.length - 4), off);\n const alreadyDb = new RegExp('\\\\[' + db + '\\\\]\\\\.$', 'i').test(before);\n if (alreadyDb) return m;\n return '[' + tenantName + '].[dbo].[' + cleanName(t) + ']';\n });\n\n // DİKKAT: Köşeli parantez içindeki her şeyi temizleme.\n // Aliaslar [Adı], [Üst Adı], [Çalışma Saati] gibi Türkçe/boşluklu olabilir.\n // Global bracket temizliği aliasları [] haline getirir.\n return q.replace(/\\s+/g, ' ').trim();\n}\n\nfunction qualifyTables(sql) {\n let q = canonicalizeSql(sql);\n const tables = Array.from(allowedTables).sort((a, b) => b.length - a.length);\n\n for (const table of tables) {\n const t = escapeRegExp(table);\n\n // FROM dbo.Table / JOIN dbo.Table\n q = q.replace(new RegExp('\\\\b(FROM|JOIN)\\\\s+dbo\\\\.\\\\[?' + t + '\\\\]?', 'gi'), '$1 [' + tenantName + '].[dbo].[' + table + ']');\n\n // FROM [dbo].[Table] / JOIN [dbo].[Table]\n q = q.replace(new RegExp('\\\\b(FROM|JOIN)\\\\s+\\\\[dbo\\\\]\\\\.\\\\[' + t + '\\\\]', 'gi'), '$1 [' + tenantName + '].[dbo].[' + table + ']');\n\n // FROM Table / JOIN Table veya FROM [Table] / JOIN [Table]\n q = q.replace(new RegExp('\\\\b(FROM|JOIN)\\\\s+\\\\[?' + t + '\\\\]?(?=\\\\s|$)', 'gi'), '$1 [' + tenantName + '].[dbo].[' + table + ']');\n }\n\n return canonicalizeSql(q);\n}\n\nfunction extractTables(sql) {\n const tables = [];\n const re = /\\b(?:FROM|JOIN)\\s+((?:\\[[^\\]]+\\]\\.){0,3}\\[[^\\]]+\\]|(?:[A-Za-z0-9_]+\\.){0,3}[A-Za-z0-9_]+)/gi;\n let m;\n while ((m = re.exec(sql)) !== null) {\n const parts = String(m[1]).split('.');\n tables.push(cleanName(parts[parts.length - 1]));\n }\n return tables;\n}\n\nfunction validateCandidate(sqlText, sourceName) {\n let sql = cleanSql(sqlText);\n if (!sql) return { ok: false, error: 'SELECT bulunamadı' };\n if (!/^SELECT\\b/i.test(sql)) return { ok: false, error: 'Üretilen çıktı SELECT ile başlamıyor.' };\n\n const forbidden = /\\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|TRUNCATE|CREATE|EXEC|EXECUTE|GRANT|REVOKE|BACKUP|RESTORE)\\b/i;\n if (forbidden.test(sql)) {\n return { ok: false, error: 'Güvenlik nedeniyle sadece SELECT sorgularına izin veriliyor.' };\n }\n\n sql = qualifyTables(sql);\n sql = cleanSql(sql);\n\n if (/LookupQuery\\s*:/i.test(sql)) {\n return { ok: false, error: 'SQL içinde hatalı LookupQuery kolon formatı var.', cleanedText: sql };\n }\n\n const doubleDbRe = new RegExp('\\\\[' + escapeRegExp(tenantName) + '\\\\]\\\\.\\\\[' + escapeRegExp(tenantName) + '\\\\]\\\\.\\\\[dbo\\\\]', 'i');\n if (doubleDbRe.test(sql)) {\n return { ok: false, error: 'SQL içinde çift database formatı var.', cleanedText: sql };\n }\n\n const usedTables = extractTables(sql);\n const unknown = usedTables.filter(t => !allowedTables.has(cleanName(t)));\n if (unknown.length) {\n return { ok: false, error: 'Veri sözlüğünde olmayan tablo kullanıldı.', unknownTables: unknown, usedTables, cleanedText: sql };\n }\n\n return { ok: true, sql, usedTables, sourceName };\n}\n\n// Önemli: Model bazen doğru SQL'i yorumluyor/eleştiriyor. Bu yüzden önce deterministik directSql kullanılır.\nlet candidate = build.directSql\n ? validateCandidate(build.directSql, 'directSql')\n : { ok: false, error: 'directSql yok' };\n\n// directSql yoksa veya geçersizse Ollama çıktısı denenir.\nif (!candidate.ok) {\n candidate = validateCandidate(raw, 'ollama');\n}\n\nif (!candidate.ok) {\n return [{\n json: {\n valid: false,\n type: 'sql_error',\n question,\n error: candidate.error || 'SQL doğrulanamadı.',\n unknownTables: candidate.unknownTables || null,\n allowedTables: Array.from(allowedTables),\n generatedText: raw,\n cleanedText: candidate.cleanedText || null,\n directSql: build.directSql || null,\n selectedFormCode: build.selectedFormCode,\n candidateFormCodes: build.candidateFormCodes\n }\n }];\n}\n\nreturn [{\n json: {\n valid: true,\n type: 'sql',\n question,\n tenantName,\n sql: candidate.sql,\n sqlSource: candidate.sourceName,\n generatedText: raw,\n directSql: build.directSql,\n usedTables: candidate.usedTables,\n dictionaryMeta: build.dictionaryMeta,\n candidateFormCodes: build.candidateFormCodes,\n selectedFormCode: build.selectedFormCode\n }\n}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1130, - 40 + -30864, + -3360 ], - "id": "07579d4a-b1ef-4a8f-b8d6-f646af9c938f", + "id": "8d752e56-0e49-4bc9-8af4-4dade50bc690", "name": "Validate SQL" }, { @@ -220,11 +227,11 @@ "caseSensitive": true, "leftValue": "", "typeValidation": "strict", - "version": 2 + "version": 1 }, "conditions": [ { - "id": "f4c65cdb-f275-42a6-971c-ea1f62d9ff87", + "id": "c6195621-ef9a-4298-9317-ae22e6c86b92", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { @@ -241,10 +248,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ - 1350, - 40 + -30640, + -3360 ], - "id": "4f189011-b12c-465e-8de0-72c9bc1457ef", + "id": "8a653bdf-3fc8-4cba-af8d-d3c827dfd739", "name": "SQL Valid?" }, { @@ -255,36 +262,42 @@ "type": "n8n-nodes-base.microsoftSql", "typeVersion": 1.1, "position": [ - 1580, - -60 + -30416, + -3456 ], - "id": "a2ec036a-e23b-4904-89aa-766523f0dd35", - "name": "Execute Generated SQL" + "id": "13a61ae4-27e2-4b0b-ab81-636281d81812", + "name": "Execute Generated SQL", + "credentials": { + "microsoftSql": { + "id": "FV4335uaFWUGPlIf", + "name": "Microsoft SQL account" + } + } }, { "parameters": { - "jsCode": "const rows = $input.all().map(i => i.json);\nconst validate = $('Validate SQL').first().json || {};\nconst columns = rows.length ? Object.keys(rows[0]) : [];\nreturn [{ json: { type: 'sql_result', question: validate.question, sql: validate.sql, columns, rows, rowCount: rows.length } }];" + "jsCode": "\nconst rows = $input.all().map(item => item.json);\nconst v = $('Validate SQL').first().json || {};\nreturn [{ json: {\n type: 'sql_result',\n question: v.question,\n sql: v.sql,\n rowCount: rows.length,\n columns: rows.length > 0 ? Object.keys(rows[0]) : [],\n rows,\n debug: { usedTables: v.usedTables, candidateFormCodes: v.candidateFormCodes, dictionaryMeta: v.dictionaryMeta }\n}}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1810, - -60 + -30176, + -3456 ], - "id": "76a92057-357a-4acf-87f9-9bd52a55b85a", + "id": "c6325122-ac66-4ecc-845b-61d8b8b9a0c2", "name": "Format SQL Result" }, { "parameters": { - "jsCode": "const e = $input.first().json || {};\nreturn [{ json: { type: 'sql_error', question: e.question, error: e.error || 'SQL üretilemedi.', generatedText: e.generatedText || null, debug: { dictionaryAvailable: e.dictionaryAvailable, dictionaryPreview: e.dictionaryPreview, ollamaRawOutput: e.ollamaRawOutput || null } } }];" + "jsCode": "\nconst e = $input.first().json || {};\nreturn [{ json: { type: e.type || 'sql_error', question: e.question || $('Webhook').first().json.body?.question || '', error: e.error || 'SQL üretilemedi.', generatedText: e.generatedText || null, cleanedText: e.cleanedText || null, unknownTables: e.unknownTables || null, allowedTables: e.allowedTables || null } }];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1580, - 160 + -30416, + -3216 ], - "id": "c34f8a67-2b5c-4700-84fd-1171a4ce6485", + "id": "6969c1e6-1c67-4305-a4c3-937e66ce3dea", "name": "SQL Error Response" } ], @@ -338,7 +351,7 @@ "main": [ [ { - "node": "Build Data Dictionary Query", + "node": "Build Dictionary Query", "type": "main", "index": 0 } @@ -374,18 +387,18 @@ ] ] }, - "Build Data Dictionary Query": { + "Build Dictionary Query": { "main": [ [ { - "node": "Get Data Dictionary Prompt", + "node": "Get Dictionary JSON", "type": "main", "index": 0 } ] ] }, - "Get Data Dictionary Prompt": { + "Get Dictionary JSON": { "main": [ [ { @@ -459,14 +472,18 @@ ] } }, - "active": false, + "active": true, "settings": { - "executionOrder": "v1" + "executionOrder": "v1", + "availableInMCP": false, + "timeSavedMode": "fixed", + "callerPolicy": "workflowsFromSameOwner" }, - "versionId": "074ecf8d-3021-4e36-b8cb-5fac6d27f68f", + "versionId": "22ff5d0b-ab86-47fb-8251-c38258384419", "meta": { - "templateCredsSetupCompleted": false + "templateCredsSetupCompleted": true, + "instanceId": "1d288821beaaeeada5e8dce6f282c802098a0c83ef6ddb35a174b09a9d43850e" }, - "id": "ChatSQLDataDictionaryHTTPStableV2", + "id": "MgXO29wLFA4urmNk", "tags": [] } \ No newline at end of file diff --git a/ui/src/views/ai/Assistant.tsx b/ui/src/views/ai/Assistant.tsx index 174a31f..32f48b3 100644 --- a/ui/src/views/ai/Assistant.tsx +++ b/ui/src/views/ai/Assistant.tsx @@ -12,7 +12,7 @@ import { MessageContent, SqlResultRow, } from '@/proxy/ai/models' -import { Container } from '@/components/shared' +import { Container, Loading } from '@/components/shared' import { Helmet } from 'react-helmet' import { APP_NAME } from '@/constants/app.constant' import dayjs from 'dayjs' @@ -421,7 +421,9 @@ const Assistant = () => { {sql && (
- Oluşturulan SQL sorgusunu göster ({rowCount ?? resultRows.length} kayıt) + + Oluşturulan SQL sorgusunu göster ({rowCount ?? resultRows.length} kayıt) + + @@ -439,7 +441,7 @@ const Assistant = () => { ) : (
- + {resultColumns.map((column) => (
{ ))} - {loading && ( -
-
- {translate('::AI.WaitAnswer')} -
- )} +