diff --git a/.all-contributorsrc b/.all-contributorsrc index 8909956d..8e875c59 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -120,6 +120,15 @@ "contributions": [ "code" ] + }, + { + "login": "techgarage-ir", + "name": "Tech Garage", + "avatar_url": "https://avatars.githubusercontent.com/u/126519308?v=4", + "profile": "https://techgarage.ir/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 10 diff --git a/.github/workflows/DotNET-build.yml b/.github/workflows/DotNET-build.yml index 8819d367..41953c96 100644 --- a/.github/workflows/DotNET-build.yml +++ b/.github/workflows/DotNET-build.yml @@ -36,11 +36,11 @@ jobs: with: distribution: 'temurin' java-version: '17' - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 'Cache: .nuke/temp, ~/.nuget/packages' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .nuke/temp diff --git a/.github/workflows/JS-build.yml b/.github/workflows/JS-build.yml index f9f8b72f..36e0f6f0 100644 --- a/.github/workflows/JS-build.yml +++ b/.github/workflows/JS-build.yml @@ -36,11 +36,11 @@ jobs: with: distribution: 'temurin' java-version: '17' - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 'Cache: .nuke/temp, ~/.nuget/packages' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .nuke/temp diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 0919c0a5..3da37125 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -48,11 +48,11 @@ jobs: with: distribution: 'temurin' java-version: '17' - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 'Cache: .nuke/temp, ~/.nuget/packages' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .nuke/temp diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 12486dce..7a201556 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,68 +1,119 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/build", - "title": "Build Schema", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "ElasticProvider": { + "type": "string" + }, + "MongoProvider": { + "type": "string" + }, + "MsSqlProvider": { + "type": "string" + }, + "MySqlProvider": { + "type": "string" + }, + "NugetApiKey": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "PostgresProvider": { + "type": "string" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "SonarToken": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "SonarTokenUi": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Ui": { + "type": "string" + } + }, "definitions": { - "build": { - "type": "object", + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "Backend_Clean", + "Backend_Compile", + "Backend_Report_Ci", + "Backend_Restore", + "Backend_SonarScan_End", + "Backend_SonarScan_Start", + "Backend_Test", + "Backend_Test_Ci", + "Clean", + "Frontend_Build", + "Frontend_Clean", + "Frontend_Restore", + "Frontend_Tests", + "Frontend_Tests_Ci", + "Pack", + "Publish" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { "properties": { - "Configuration": { - "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", - "enum": [ - "Debug", - "Release" - ] - }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, - "ElasticProvider": { - "type": "string" - }, "Help": { "type": "boolean", "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] - }, - "MongoProvider": { - "type": "string" - }, - "MsSqlProvider": { - "type": "string" - }, - "MySqlProvider": { - "type": "string" + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", "description": "Disables displaying the NUKE logo" }, - "NugetApiKey": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Partition": { "type": "string", "description": "Partition to use on CI" @@ -71,9 +122,6 @@ "type": "boolean", "description": "Shows the execution plan (HTML)" }, - "PostgresProvider": { - "type": "string" - }, "Profile": { "type": "array", "description": "Defines the profiles to load", @@ -89,78 +137,22 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "Backend_Clean", - "Backend_Compile", - "Backend_Report_Ci", - "Backend_Restore", - "Backend_SonarScan_End", - "Backend_SonarScan_Start", - "Backend_Test", - "Backend_Test_Ci", - "Clean", - "Frontend_Build", - "Frontend_Clean", - "Frontend_Restore", - "Frontend_Tests", - "Frontend_Tests_Ci", - "Pack", - "Publish" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, - "SonarToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "SonarTokenUi": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Backend_Clean", - "Backend_Compile", - "Backend_Report_Ci", - "Backend_Restore", - "Backend_SonarScan_End", - "Backend_SonarScan_Start", - "Backend_Test", - "Backend_Test_Ci", - "Clean", - "Frontend_Build", - "Frontend_Clean", - "Frontend_Restore", - "Frontend_Tests", - "Frontend_Tests_Ci", - "Pack", - "Publish" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Ui": { - "type": "string" - }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$ref": "#/definitions/Verbosity" } } } - } + }, + "$ref": "#/definitions/NukeBuild" } diff --git a/Directory.Build.props b/Directory.Build.props index bf94e62c..43a15681 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/README.md b/README.md index fdf1eff1..8d643448 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ A simple Serilog log viewer for the following sinks: - Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver)) -- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) +- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) - Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql)) and Serilog.Sinks.**Postgresql.Alternative** ([Nuget](https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative)) - Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb)) - Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch)) - Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb)) +- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/)) @@ -43,6 +44,7 @@ Install one or more of the available providers, based upon your sink(s): | **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` | | **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` | | **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` | +| **Serilog.UI.SQLiteProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.SQLiteProvider)] | `dotnet add package Serilog.UI.SQLiteProvider` | `Install-Package Serilog.UI.SQLiteProvider` | ### DI registration @@ -120,6 +122,7 @@ See [LICENSE](https://github.com/serilog-contrib/serilog-ui/blob/master/LICENSE) Uthman
Uthman

💻 jorgevp
jorgevp

💻 + Tech Garage
Tech Garage

💻 diff --git a/README_Nuget.md b/README_Nuget.md index a3f29621..cae30e94 100644 --- a/README_Nuget.md +++ b/README_Nuget.md @@ -3,11 +3,12 @@ A simple Serilog log viewer for the following sinks: - Serilog.Sinks.**MSSqlServer** ([Nuget](https://github.com/serilog/serilog-sinks-mssqlserver)) -- Serilog.Sinks.**MySql** ([Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) +- Serilog.Sinks.**MySql** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-mysql)) and Serilog.Sinks.**MariaDB** [Nuget](https://github.com/TeleSoftas/serilog-sinks-mariadb) - Serilog.Sinks.**Postgresql** ([Nuget](https://github.com/b00ted/serilog-sinks-postgresql)) and Serilog.Sinks.**Postgresql.Alternative** ([Nuget](https://github.com/serilog-contrib/Serilog.Sinks.Postgresql.Alternative)) - Serilog.Sinks.**MongoDB** ([Nuget](https://github.com/serilog/serilog-sinks-mongodb)) - Serilog.Sinks.**ElasticSearch** ([Nuget](https://github.com/serilog/serilog-sinks-elasticsearch)) - Serilog.Sinks.**RavenDB** ([Nuget](https://github.com/ravendb/serilog-sinks-ravendb)) +- Serilog.Sinks.**SQLite** ([Nuget](https://github.com/saleem-mirza/serilog-sinks-sqlite/)) # Read the [Wiki](https://github.com/serilog-contrib/serilog-ui/wiki) @@ -35,6 +36,7 @@ Install one or more of the available providers, based upon your sink(s): | **Serilog.UI.MongoDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.MongoDbProvider)] | `dotnet add package Serilog.UI.MongoDbProvider` | `Install-Package Serilog.UI.MongoDbProvider` | | **Serilog.UI.ElasticSearchProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.ElasticSearchProvider)] | `dotnet add package Serilog.UI.ElasticSearchProvider` | `Install-Package Serilog.UI.ElasticSearchProvider` | | **Serilog.UI.RavenDbProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.RavenDbProvider)] | `dotnet add package Serilog.UI.RavenDbProvider` | `Install-Package Serilog.UI.RavenDbProvider` | +| **Serilog.UI.SQLiteProvider** [[NuGet](https://www.nuget.org/packages/Serilog.UI.SQLiteProvider)] | `dotnet add package Serilog.UI.SQLiteProvider` | `Install-Package Serilog.UI.SQLiteProvider` | ### DI registration diff --git a/Serilog.Ui.sln b/Serilog.Ui.sln index 6fa13f8a..8fd5e62c 100644 --- a/Serilog.Ui.sln +++ b/Serilog.Ui.sln @@ -61,6 +61,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.RavenDbProvider.Tests", "tests\Serilog.Ui.RavenDbProvider.Tests\Serilog.Ui.RavenDbProvider.Tests.csproj", "{B785845B-D858-4562-B224-67468B4FEE41}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteDataProvider", "src\Serilog.Ui.SqliteDataProvider\Serilog.Ui.SqliteDataProvider.csproj", "{A23F4275-DB47-40C9-96CE-1116E20F5EB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Ui.SqliteProvider.Tests", "tests\Serilog.Ui.SqliteProvider.Tests\Serilog.Ui.SqliteProvider.Tests.csproj", "{C9CBABEA-622C-4E11-9D68-816F685E8E0D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp", "samples\WebApp\WebApp.csproj", "{8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "samples\WebApi\WebApi.csproj", "{A2701899-102D-4926-B054-FD76F59A0791}" @@ -137,6 +141,14 @@ Global {B785845B-D858-4562-B224-67468B4FEE41}.Debug|Any CPU.Build.0 = Debug|Any CPU {B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.ActiveCfg = Release|Any CPU {B785845B-D858-4562-B224-67468B4FEE41}.Release|Any CPU.Build.0 = Release|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A23F4275-DB47-40C9-96CE-1116E20F5EB7}.Release|Any CPU.Build.0 = Release|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9CBABEA-622C-4E11-9D68-816F685E8E0D}.Release|Any CPU.Build.0 = Release|Any CPU {8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F9A0E5E-8C1D-4FF8-8865-1B362CF51765}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -168,6 +180,8 @@ Global {DCB452AD-2E0E-4D6A-B46D-72D0AF247381} = {83E91BE7-19B3-4AE0-992C-9DFF30FC409E} {8973E5F5-FD9B-41B1-B2D6-8B281754C443} = {ACA69857-2E3E-468C-B0B0-A86852E3492D} {B785845B-D858-4562-B224-67468B4FEE41} = {75F9223B-15F2-4465-B01D-2A5A49FCD000} + {A23F4275-DB47-40C9-96CE-1116E20F5EB7} = {ACA69857-2E3E-468C-B0B0-A86852E3492D} + {C9CBABEA-622C-4E11-9D68-816F685E8E0D} = {75F9223B-15F2-4465-B01D-2A5A49FCD000} {8F9A0E5E-8C1D-4FF8-8865-1B362CF51765} = {157CA77C-513A-409F-8045-E68739AAC8C8} {A2701899-102D-4926-B054-FD76F59A0791} = {157CA77C-513A-409F-8045-E68739AAC8C8} EndGlobalSection diff --git a/Serilog.Ui.sln.DotSettings b/Serilog.Ui.sln.DotSettings index 4d35b8be..e62a5d60 100644 --- a/Serilog.Ui.sln.DotSettings +++ b/Serilog.Ui.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/build/_build.csproj b/build/_build.csproj index 65b26c59..0027e160 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/samples/WebApi/WebApi.csproj b/samples/WebApi/WebApi.csproj index 0ee86da5..574a2a7a 100644 --- a/samples/WebApi/WebApi.csproj +++ b/samples/WebApi/WebApi.csproj @@ -14,7 +14,7 @@ - + @@ -22,9 +22,9 @@ diff --git a/samples/WebApp/WebApp.csproj b/samples/WebApp/WebApp.csproj index ed8b0fa2..4771bc46 100644 --- a/samples/WebApp/WebApp.csproj +++ b/samples/WebApp/WebApp.csproj @@ -18,7 +18,7 @@ - + @@ -38,8 +38,8 @@ diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs new file mode 100644 index 00000000..786c1d1a --- /dev/null +++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs @@ -0,0 +1,37 @@ +namespace Serilog.Ui.Core.QueryBuilder.Sql; + +/// +/// Represents the column names used in the SQL-based sink for logging. +/// +public abstract class SinkColumnNames +{ + /// + /// Gets or sets the message of the log entry. + /// + public string Message { get; set; } = string.Empty; + + /// + /// Gets or sets the message template of the log entry. + /// + public string MessageTemplate { get; set; } = string.Empty; + + /// + /// Gets or sets the level of the log entry. + /// + public string Level { get; set; } = string.Empty; + + /// + /// Gets or sets the timestamp of the log entry. + /// + public string Timestamp { get; set; } = string.Empty; + + /// + /// Gets or sets the exception of the log entry. + /// + public string Exception { get; set; } = string.Empty; + + /// + /// Gets or sets the serialized log event like properties. + /// + public string LogEventSerialized { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs new file mode 100644 index 00000000..b9b112b3 --- /dev/null +++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs @@ -0,0 +1,67 @@ +using Serilog.Ui.Core.Attributes; +using Serilog.Ui.Core.Models; +using System.Reflection; +using static Serilog.Ui.Core.Models.SearchOptions; + +namespace Serilog.Ui.Core.QueryBuilder.Sql; + +/// +/// Abstract class that provides methods to build SQL queries for fetching and counting logs. +/// +public abstract class SqlQueryBuilder where TModel : LogModel +{ + /// + /// Builds a SQL query to fetch logs from the specified table. + /// + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for fetching logs. + /// A SQL query string to fetch logs. + public abstract string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query); + + /// + /// Builds a SQL query to count logs in the specified table. + /// + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public abstract string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query); + + /// + /// Generates a SQL sort clause based on the specified sort property and direction. + /// + /// The column names used in the sink for logging. + /// The property to sort on. + /// The direction to sort by. + /// A SQL sort clause string. + protected abstract string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy); + + /// + /// Generates a SQL sort clause based on the specified sort property and direction. + /// + /// The column names used in the sink for logging. + /// The property to sort on. + /// A SQL sort clause string. + protected static string GetSortColumnName(SinkColumnNames columns, SortProperty sortOn) => sortOn switch + { + SortProperty.Timestamp => columns.Timestamp, + SortProperty.Level => columns.Level, + SortProperty.Message => columns.Message, + _ => columns.Timestamp + }; + + /// + /// Determines whether to add the exception column to the WHERE clause based on the presence of the RemovedColumnAttribute. + /// + /// True if the exception column should be added to the WHERE clause; otherwise, false. + protected static bool AddExceptionToWhereClause() + { + PropertyInfo? exceptionProperty = typeof(TModel).GetProperty("Exception"); + RemovedColumnAttribute? att = exceptionProperty?.GetCustomAttribute(); + + return att is null; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj b/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj index cf49d99e..4cb6fb32 100644 --- a/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj +++ b/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj @@ -3,12 +3,12 @@ netstandard2.0 True latest - 3.0.0 + 3.1.0 - + - + \ No newline at end of file diff --git a/src/Serilog.Ui.ElasticSearchProvider/Serilog.Ui.ElasticSearchProvider.csproj b/src/Serilog.Ui.ElasticSearchProvider/Serilog.Ui.ElasticSearchProvider.csproj index a5be13be..bfd5db6e 100644 --- a/src/Serilog.Ui.ElasticSearchProvider/Serilog.Ui.ElasticSearchProvider.csproj +++ b/src/Serilog.Ui.ElasticSearchProvider/Serilog.Ui.ElasticSearchProvider.csproj @@ -5,7 +5,7 @@ netstandard2.0 latest - 3.0.0 + 3.1.0 Ricardo Demauro - rmauro.dev ElasticSearch data provider for Serilog UI. diff --git a/src/Serilog.Ui.MongoDbProvider/Serilog.Ui.MongoDbProvider.csproj b/src/Serilog.Ui.MongoDbProvider/Serilog.Ui.MongoDbProvider.csproj index 988835d3..8b3898a1 100644 --- a/src/Serilog.Ui.MongoDbProvider/Serilog.Ui.MongoDbProvider.csproj +++ b/src/Serilog.Ui.MongoDbProvider/Serilog.Ui.MongoDbProvider.csproj @@ -4,7 +4,7 @@ Serilog.UI.MongoDbProvider netstandard2.0 latest - 3.0.0 + 3.1.0 Christian Haase MongoDB data provider for Serilog UI. diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index a1d67b01..30f36029 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,64 +1,64 @@ -using System; -using Dapper; +using Dapper; using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Core.Models.Options; +using System; -namespace Serilog.Ui.MsSqlServerProvider.Extensions -{ - /// - /// SQL Server data provider specific extension methods for . - /// - public static class SerilogUiOptionBuilderExtensions - { - /// Configures the SerilogUi to connect to a SQL Server database. - /// The options builder. - /// The Ms Sql options action. - /// - /// Delegate to customize the DateTime parsing. - /// It throws if the return DateTime isn't UTC kind. - /// - public static ISerilogUiOptionsBuilder UseSqlServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions, - Func? dateTimeCustomParsing = null - ) => optionsBuilder.UseSqlServer(setupOptions, dateTimeCustomParsing); - - /// Configures the SerilogUi to connect to a SQL Server database. - /// The log model, containing any additional columns. It must inherit . - /// The options builder. - /// The Ms Sql options action. - /// - /// Delegate to customize the DateTime parsing. - /// It throws if the return DateTime isn't UTC kind. - /// - public static ISerilogUiOptionsBuilder UseSqlServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions, - Func? dateTimeCustomParsing = null - ) where T : SqlServerLogModel - { - var dbOptions = new RelationalDbOptions("dbo"); - setupOptions(dbOptions); - dbOptions.Validate(); - - var providerName = dbOptions.GetProviderName(SqlServerDataProvider.MsSqlProviderName); +namespace Serilog.Ui.MsSqlServerProvider.Extensions; - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - SqlMapper.AddTypeHandler(new DapperDateTimeHandler(dateTimeCustomParsing)); +/// +/// SQL Server data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions +{ + /// Configures the SerilogUi to connect to a SQL Server database. + /// The options builder. + /// The Ms Sql options action. + /// + /// Delegate to customize the DateTime parsing. + /// It throws if the return DateTime isn't UTC kind. + /// + public static ISerilogUiOptionsBuilder UseSqlServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions, + Func? dateTimeCustomParsing = null + ) => optionsBuilder.UseSqlServer(setupOptions, dateTimeCustomParsing); - var customModel = typeof(T) != typeof(SqlServerLogModel); - if (customModel) - { - optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions)); + /// Configures the SerilogUi to connect to a SQL Server database. + /// The log model, containing any additional columns. It must inherit . + /// The options builder. + /// The Ms Sql options action. + /// + /// Delegate to customize the DateTime parsing. + /// It throws if the return DateTime isn't UTC kind. + /// + public static ISerilogUiOptionsBuilder UseSqlServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions, + Func? dateTimeCustomParsing = null + ) where T : SqlServerLogModel + { + SqlServerDbOptions dbOptions = new("dbo"); + setupOptions(dbOptions); + dbOptions.Validate(); - return optionsBuilder; - } + string providerName = dbOptions.GetProviderName(SqlServerDataProvider.MsSqlProviderName); + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + SqlMapper.AddTypeHandler(new DapperDateTimeHandler(dateTimeCustomParsing)); - optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions)); - return optionsBuilder; + bool customModel = typeof(T) != typeof(SqlServerLogModel); + if (customModel) + { + optionsBuilder.RegisterColumnsInfo(providerName); + optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions, new SqlServerQueryBuilder())); } + else + { + optionsBuilder.Services.AddScoped(_ => + new SqlServerDataProvider(dbOptions, new SqlServerQueryBuilder())); + } + + return optionsBuilder; } } \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs new file mode 100644 index 00000000..2e221ec4 --- /dev/null +++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.MsSqlServerProvider.Models; + +namespace Serilog.Ui.MsSqlServerProvider.Extensions; + +public class SqlServerDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) +{ + public SinkColumnNames ColumnNames { get; } = new SqlServerSinkColumnNames(); +} \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerLogModel.cs b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerLogModel.cs similarity index 100% rename from src/Serilog.Ui.MsSqlServerProvider/SqlServerLogModel.cs rename to src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerLogModel.cs diff --git a/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs new file mode 100644 index 00000000..3806f527 --- /dev/null +++ b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.MsSqlServerProvider.Models; + +internal class SqlServerSinkColumnNames : SinkColumnNames +{ + public SqlServerSinkColumnNames() + { + Exception = "Exception"; + Level = "Level"; + LogEventSerialized = "Properties"; + Message = "Message"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj index ff8363bd..c01d0388 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj +++ b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj @@ -5,7 +5,7 @@ netstandard2.0 latest - 3.0.0 + 3.1.0 Microsoft SQL Server data provider for Serilog UI. serilog serilog-ui serilog.sinks.mssqlserver mssqlserver @@ -18,5 +18,6 @@ + \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs index 15100abe..93b57651 100644 --- a/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs +++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs @@ -1,168 +1,76 @@ -using System.Collections.Generic; +using Dapper; +using Microsoft.Data.SqlClient; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MsSqlServerProvider.Extensions; +using System.Collections.Generic; using System.Data; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Ardalis.GuardClauses; -using Dapper; -using Microsoft.Data.SqlClient; -using Serilog.Ui.Core; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; -using static Serilog.Ui.Core.Models.SearchOptions; - -namespace Serilog.Ui.MsSqlServerProvider -{ - public class SqlServerDataProvider(RelationalDbOptions options) : SqlServerDataProvider(options) - { - protected override string SearchCriteriaWhereQuery() => "OR [Exception] LIKE @Search"; - - protected override string SelectQuery() - { - const string level = $"[{ColumnLevelName}]"; - const string message = $"[{ColumnMessageName}]"; - const string timestamp = $"[{ColumnTimestampName}]"; - - return $"SELECT [Id], {message}, {level}, {timestamp}, [Exception], [Properties] "; - } - } - - public class SqlServerDataProvider(RelationalDbOptions options) : IDataProvider - where T : SqlServerLogModel - { - internal const string MsSqlProviderName = "MsSQL"; - - private protected const string ColumnTimestampName = "TimeStamp"; - - private protected const string ColumnLevelName = "Level"; - - private protected const string ColumnMessageName = "Message"; - - private readonly RelationalDbOptions _options = Guard.Against.Null(options); - - public string Name => _options.GetProviderName(MsSqlProviderName); - - protected virtual string SelectQuery() => "SELECT * "; - public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) - { - // since sink stores dates in local time, we query by local time - queryParams.ToLocalDates(); +namespace Serilog.Ui.MsSqlServerProvider; - var logsTask = GetLogsAsync(queryParams); - var logCountTask = CountLogsAsync(queryParams); +/// +public class SqlServerDataProvider(SqlServerDbOptions options, SqlServerQueryBuilder queryBuilder) + : SqlServerDataProvider(options, queryBuilder); - await Task.WhenAll(logsTask, logCountTask); - - return (await logsTask, await logCountTask); - } - - private async Task> GetLogsAsync(FetchLogsQuery queryParams) - { - var queryBuilder = new StringBuilder(); - - queryBuilder.Append(SelectQuery()); - queryBuilder.Append($"FROM [{_options.Schema}].[{_options.TableName}] "); - - GenerateWhereClause(queryBuilder, queryParams); - - GenerateSortClause(queryBuilder, queryParams.SortOn, queryParams.SortBy); - - queryBuilder.Append("OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY"); - - var rowNoStart = queryParams.Page * queryParams.Count; - - using IDbConnection connection = new SqlConnection(_options.ConnectionString); - var logs = await connection.QueryAsync(queryBuilder.ToString(), - new - { - Offset = rowNoStart, - queryParams.Count, - queryParams.Level, - Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, - queryParams.StartDate, - queryParams.EndDate - }); - - return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); - } - - private async Task CountLogsAsync(FetchLogsQuery queryParams) - { - var queryBuilder = new StringBuilder(); - queryBuilder.Append($"SELECT COUNT(Id) FROM [{_options.Schema}].[{_options.TableName}]"); +/// +public class SqlServerDataProvider(SqlServerDbOptions options, SqlServerQueryBuilder queryBuilder) : IDataProvider + where T : SqlServerLogModel +{ + internal const string MsSqlProviderName = "MsSQL"; - GenerateWhereClause(queryBuilder, queryParams); + /// + public string Name => options.GetProviderName(MsSqlProviderName); - using IDbConnection connection = new SqlConnection(_options.ConnectionString); - return await connection.ExecuteScalarAsync(queryBuilder.ToString(), - new - { - queryParams.Level, - Search = queryParams.SearchCriteria != null ? "%" + queryParams.SearchCriteria + "%" : null, - queryParams.StartDate, - queryParams.EndDate - }); - } + public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) + { + // since sink stores dates in local time, we query by local time + queryParams.ToLocalDates(); - /// - /// If Exception property is flagged with , - /// it removes the Where query part on the Exception field. - /// - /// - protected virtual string SearchCriteriaWhereQuery() - { - var exceptionProperty = typeof(T).GetProperty(nameof(SqlServerLogModel.Exception)); - var att = exceptionProperty?.GetCustomAttribute(); - return att is null ? "OR [Exception] LIKE @Search" : string.Empty; - } + var logsTask = GetLogsAsync(queryParams); + var logCountTask = CountLogsAsync(queryParams); - private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams) - { - var conditionStart = "WHERE"; + await Task.WhenAll(logsTask, logCountTask); - if (!string.IsNullOrWhiteSpace(queryParams.Level)) - { - queryBuilder.Append($"{conditionStart} [{ColumnLevelName}] = @Level "); - conditionStart = "AND"; - } + return (await logsTask, await logCountTask); + } - if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria)) - { - queryBuilder.Append($"{conditionStart} [{ColumnMessageName}] LIKE @Search {SearchCriteriaWhereQuery()} "); - conditionStart = "AND"; - } + private async Task> GetLogsAsync(FetchLogsQuery queryParams) + { + string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); + int rowNoStart = queryParams.Page * queryParams.Count; - if (queryParams.StartDate != null) - { - queryBuilder.Append($"{conditionStart} [{ColumnTimestampName}] >= @StartDate "); - conditionStart = "AND"; - } + using IDbConnection connection = new SqlConnection(options.ConnectionString); - if (queryParams.EndDate != null) + IEnumerable logs = await connection.QueryAsync(query, + new { - queryBuilder.Append($"{conditionStart} [{ColumnTimestampName}] <= @EndDate "); - } - } + Offset = rowNoStart, + queryParams.Count, + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + queryParams.StartDate, + queryParams.EndDate + }); + + return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList(); + } - private static void GenerateSortClause(StringBuilder queryBuilder, SortProperty sortOn, SortDirection sortBy) - { - var sortOnCol = GetColumnName(sortOn); - var sortByCol = sortBy.ToString().ToUpper(); + private async Task CountLogsAsync(FetchLogsQuery queryParams) + { + string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); - queryBuilder.Append($"ORDER BY [{sortOnCol}] {sortByCol} "); - } + using IDbConnection connection = new SqlConnection(options.ConnectionString); - private static string GetColumnName(SortProperty sortOn) - => sortOn switch + return await connection.ExecuteScalarAsync(query, + new { - SortProperty.Level => ColumnLevelName, - SortProperty.Message => ColumnMessageName, - SortProperty.Timestamp => ColumnTimestampName, - _ => ColumnTimestampName - }; + queryParams.Level, + Search = queryParams.SearchCriteria != null ? "%" + queryParams.SearchCriteria + "%" : null, + queryParams.StartDate, + queryParams.EndDate + }); } } \ No newline at end of file diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs new file mode 100644 index 00000000..1bf038d5 --- /dev/null +++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs @@ -0,0 +1,126 @@ +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using System; +using System.Text; + +namespace Serilog.Ui.MsSqlServerProvider; + +/// +/// Provides methods to build SQL queries specifically for SQL Server to fetch and count logs. +/// +/// The type of the log model. +public class SqlServerQueryBuilder : SqlQueryBuilder where TModel : LogModel +{ + /// + public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + GenerateSelectClause(queryStr, columns, schema, tableName); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY"); + + return queryStr.ToString(); + } + + /// + /// Builds a SQL query to count logs in the specified table. + /// + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append("SELECT COUNT([Id]) ") + .Append($"FROM [{schema}].[{tableName}] "); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + return queryStr.ToString(); + } + + protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) + => $"ORDER BY [{GetSortColumnName(columns, sortOn)}] {sortBy.ToString().ToUpper()}"; + + /// + /// Generates the SELECT clause for the SQL query. + /// + /// The StringBuilder to append the SELECT clause to. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + { + if (typeof(TModel) != typeof(SqlServerLogModel)) + { + queryBuilder.Append("SELECT * "); + } + else + { + queryBuilder.Append("SELECT [Id], ") + .Append($"[{columns.Message}], ") + .Append($"[{columns.Level}], ") + .Append($"[{columns.Timestamp}], ") + .Append($"[{columns.Exception}], ") + .Append($"[{columns.LogEventSerialized}] "); + } + + queryBuilder.Append($"FROM [{schema}].[{tableName}] "); + } + + /// + /// Generates the WHERE clause for the SQL query. + /// + /// The StringBuilder to append the WHERE clause to. + /// The column names used in the sink for logging. + /// The log level to filter by. + /// The search criteria to filter by. + /// The start date to filter by. + /// The end date to filter by. + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + StringBuilder conditions2 = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions2.Append($"AND [{columns.Level}] = @Level "); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + conditions2.Append($"AND ([{columns.Message}] LIKE @Search "); + conditions2.Append(AddExceptionToWhereClause() ? $"OR [{columns.Exception}] LIKE @Search) " : ") "); + } + + if (startDate.HasValue) + { + conditions2.Append($"AND [{columns.Timestamp}] >= @StartDate "); + } + + if (endDate.HasValue) + { + conditions2.Append($"AND [{columns.Timestamp}] <= @EndDate "); + } + + if (conditions2.Length <= 0) + { + return; + } + + queryBuilder + .Append("WHERE 1 = 1 ") + .Append(conditions2); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs new file mode 100644 index 00000000..1f3be6c0 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs @@ -0,0 +1,11 @@ +using Serilog.Ui.MySqlProvider.Models; + +namespace Serilog.Ui.MySqlProvider.Extensions; + +public class MariaDbOptions : MySqlDbOptions +{ + public MariaDbOptions(string defaultSchemaName) : base(defaultSchemaName) + { + ColumnNames = new MariaDbSinkColumnNames(); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs new file mode 100644 index 00000000..f1e07950 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.MySqlProvider.Models; + +namespace Serilog.Ui.MySqlProvider.Extensions; + +public class MySqlDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName) +{ + internal SinkColumnNames ColumnNames { get; set; } = new MySqlSinkColumnNames(); +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 16673cc5..1348591f 100644 --- a/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,84 +1,77 @@ -using System; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.Core.Models.Options; +using System; -namespace Serilog.Ui.MySqlProvider.Extensions +namespace Serilog.Ui.MySqlProvider.Extensions; + +/// +/// MySQL data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions { /// - /// MySQL data provider specific extension methods for . + /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting + /// Serilog.Sinks.MySQL. defaults. + /// Provider expects sink to store timestamp in utc. /// - public static class SerilogUiOptionBuilderExtensions + /// The options builder. + /// The MySql options action. + public static ISerilogUiOptionsBuilder UseMySqlServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) { - /// - /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting - /// Serilog.Sinks.MySQL. defaults. - /// Provider expects sink to store timestamp in utc. - /// - /// The options builder. - /// The MySql options action. - public static ISerilogUiOptionsBuilder UseMySqlServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) - { - var dbOptions = new RelationalDbOptions("dbo"); - setupOptions(dbOptions); - dbOptions.Validate(); - - var providerName = dbOptions.GetProviderName(MySqlDataProvider.MySqlProviderName); - - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + MySqlDbOptions dbOptions = new("dbo"); + setupOptions(dbOptions); + dbOptions.Validate(); - optionsBuilder.Services.AddScoped(_ => new MySqlDataProvider(dbOptions)); + string providerName = dbOptions.GetProviderName(MySqlDataProvider.MySqlProviderName); - return optionsBuilder; - } + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + optionsBuilder.Services.AddScoped(_ => new MySqlDataProvider(dbOptions, new MySqlQueryBuilder())); - /// - /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting - /// Serilog.Sinks.MariaDB. defaults. - /// Provider expects sink to store timestamp in utc. - /// - /// The options builder. - /// The MySql options action. - public static ISerilogUiOptionsBuilder UseMariaDbServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) => optionsBuilder.UseMariaDbServer(setupOptions); + return optionsBuilder; + } - /// - /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting - /// Serilog.Sinks.MariaDB. defaults. - /// Provider expects sink to store timestamp in utc. - /// - /// The log model, containing any additional columns. It must inherit . - /// The options builder. - /// The MySql options action. - public static ISerilogUiOptionsBuilder UseMariaDbServer( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) where T : MySqlLogModel - { - var dbOptions = new RelationalDbOptions("dbo"); - setupOptions(dbOptions); - dbOptions.Validate(); + /// + /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting + /// Serilog.Sinks.MariaDB. defaults. + /// Provider expects sink to store timestamp in utc. + /// + /// The options builder. + /// The MySql options action. + public static ISerilogUiOptionsBuilder UseMariaDbServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + => optionsBuilder.UseMariaDbServer(setupOptions); - var providerName = dbOptions.GetProviderName(MariaDbDataProvider.ProviderName); + /// + /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting + /// Serilog.Sinks.MariaDB. defaults. + /// Provider expects sink to store timestamp in utc. + /// + /// The log model, containing any additional columns. It must inherit . + /// The options builder. + /// The MySql options action. + public static ISerilogUiOptionsBuilder UseMariaDbServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + where T : MySqlLogModel + { + MariaDbOptions dbOptions = new("dbo"); + setupOptions(dbOptions); + dbOptions.Validate(); - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + string providerName = dbOptions.GetProviderName(MariaDbDataProvider.ProviderName); - var customModel = typeof(T) != typeof(MySqlLogModel); - if (customModel) - { - optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions)); - return optionsBuilder; - } + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions)); - return optionsBuilder; + bool customModel = typeof(T) != typeof(MySqlLogModel); + if (customModel) + { + optionsBuilder.RegisterColumnsInfo(providerName); + optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions, new MySqlQueryBuilder())); + } + else + { + optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions, new MySqlQueryBuilder())); } + + return optionsBuilder; } } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs b/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs index 92bb825f..37fe6af3 100644 --- a/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs +++ b/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs @@ -1,23 +1,16 @@ -using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.MySqlProvider.Extensions; using Serilog.Ui.MySqlProvider.Shared; namespace Serilog.Ui.MySqlProvider; -public class MariaDbDataProvider(RelationalDbOptions options) : MariaDbDataProvider(options) -{ - protected override string SelectQuery - => $"SELECT Id, {ColumnMessageName}, {ColumnLevelName} AS 'Level', {ColumnTimestampName}, Exception, Properties "; - - protected override string SearchCriteriaWhereQuery() => "OR Exception LIKE @Search"; -} +public class MariaDbDataProvider(MariaDbOptions options, MySqlQueryBuilder queryBuilder) + : MariaDbDataProvider(options, queryBuilder); -public class MariaDbDataProvider(RelationalDbOptions options) : DataProvider(options) +public class MariaDbDataProvider(MariaDbOptions options, MySqlQueryBuilder queryBuilder) : DataProvider(options, queryBuilder) where T : MySqlLogModel { internal const string ProviderName = "MariaDb"; - protected override string ColumnLevelName => "LogLevel"; - - public override string Name => Options.GetProviderName(ProviderName); + public override string Name => options.GetProviderName(ProviderName); } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs b/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs new file mode 100644 index 00000000..723a6f0a --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.MySqlProvider.Models; + +internal class MariaDbSinkColumnNames : SinkColumnNames +{ + public MariaDbSinkColumnNames() + { + Exception = "Exception"; + Level = "LogLevel"; + LogEventSerialized = "Properties"; + Message = "Message"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs new file mode 100644 index 00000000..a79802d6 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs @@ -0,0 +1,27 @@ +using System; +using Serilog.Ui.Core.Attributes; +using Serilog.Ui.Core.Models; + +namespace Serilog.Ui.MySqlProvider; + +/// +/// MySql/MariaDb Log Model.
+/// , , , +/// columns can't be overridden and removed from the model, due to query requirements.
+/// To remove a field, apply on it. +/// To add a field, register the property with the correct datatype on the child class and the sink. +///
+public class MySqlLogModel : LogModel +{ + public override sealed int RowNo => base.RowNo; + + public override sealed string? Level { get; set; } + + public string LogLevel { get; set; } = string.Empty; + + public override sealed string? Message { get; set; } = string.Empty; + + public override sealed DateTime Timestamp { get; set; } + + public override string PropertyType => "json"; +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs new file mode 100644 index 00000000..67c6888a --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.MySqlProvider.Models; + +internal class MySqlSinkColumnNames : SinkColumnNames +{ + public MySqlSinkColumnNames() + { + Exception = "Exception"; + Level = "Level"; + LogEventSerialized = "Properties"; + Message = "Message"; + MessageTemplate = ""; + Timestamp = "TimeStamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs b/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs index 6bdd363e..b11e8dcb 100644 --- a/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs +++ b/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs @@ -1,15 +1,14 @@ -using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.MySqlProvider.Extensions; using Serilog.Ui.MySqlProvider.Shared; namespace Serilog.Ui.MySqlProvider; -public class MySqlDataProvider(RelationalDbOptions options) : DataProvider(options) +public class MySqlDataProvider(MySqlDbOptions options, MySqlQueryBuilder queryBuilder) + : DataProvider(options, queryBuilder) { - protected override string SelectQuery - => $"SELECT Id, {ColumnMessageName}, {ColumnLevelName}, {ColumnTimestampName}, Exception, Properties "; - - protected override string SearchCriteriaWhereQuery() => "OR Exception LIKE @Search"; + private readonly MySqlDbOptions _options = options; internal const string MySqlProviderName = "MySQL"; - public override string Name => Options.GetProviderName(MySqlProviderName); + + public override string Name => _options.GetProviderName(MySqlProviderName); } \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs deleted file mode 100644 index e1358aaa..00000000 --- a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; - -namespace Serilog.Ui.MySqlProvider -{ - /// - /// MySql/MariaDb Log Model.
- /// , , , - /// columns can't be overridden and removed from the model, due to query requirements.
- /// To remove a field, apply on it. - /// To add a field, register the property with the correct datatype on the child class and the sink. - ///
- public class MySqlLogModel : LogModel - { - public sealed override int RowNo => base.RowNo; - - public sealed override string? Level { get; set; } - - public string LogLevel { get; set; } = string.Empty; - - public sealed override string? Message { get; set; } = string.Empty; - - public sealed override DateTime Timestamp { get; set; } - - public override string PropertyType => "json"; - } -} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs b/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs new file mode 100644 index 00000000..b52150f1 --- /dev/null +++ b/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs @@ -0,0 +1,125 @@ +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using System; +using System.Text; + +namespace Serilog.Ui.MySqlProvider; + +/// +/// Provides methods to build SQL queries specifically for MySQL and MariaDB to fetch and count logs. +/// +/// The type of the log model. +public class MySqlQueryBuilder : SqlQueryBuilder where TModel : LogModel +{ + /// + public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + GenerateSelectClause(queryStr, columns, tableName); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Offset, @Count"); + + return queryStr.ToString(); + } + + /// + /// Builds a SQL query to count logs in the specified table. + /// + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + /// The query parameters for counting logs. + /// A SQL query string to count logs. + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append("SELECT COUNT(Id) ") + .Append($"FROM {tableName} "); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + return queryStr.ToString(); + } + + protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) + => $"ORDER BY {GetSortColumnName(columns, sortOn)} {sortBy.ToString().ToUpper()}"; + + /// + /// Generates the SELECT clause for the SQL query. + /// + /// The StringBuilder to append the SELECT clause to. + /// The column names used in the sink for logging. + /// The name of the table. + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string tableName) + { + if (typeof(TModel) != typeof(MySqlLogModel)) + { + queryBuilder.Append("SELECT * "); + } + else + { + queryBuilder.Append("SELECT Id, ") + .Append($"{columns.Message}, ") + .Append($"{columns.Level}, ") + .Append($"{columns.Timestamp}, ") + .Append($"{columns.Exception}, ") + .Append($"{columns.LogEventSerialized} "); + } + + queryBuilder.Append($"FROM {tableName} "); + } + + /// + /// Generates the WHERE clause for the SQL query. + /// + /// The StringBuilder to append the WHERE clause to. + /// The column names used in the sink for logging. + /// The log level to filter by. + /// The search criteria to filter by. + /// The start date to filter by. + /// The end date to filter by. + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + StringBuilder conditions2 = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions2.Append($"AND {columns.Level} = @Level "); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + conditions2.Append($"AND ({columns.Message} LIKE @Search "); + conditions2.Append(AddExceptionToWhereClause() ? $"OR {columns.Exception} LIKE @Search) " : ") "); + } + + if (startDate.HasValue) + { + conditions2.Append($"AND {columns.Timestamp} >= @StartDate "); + } + + if (endDate.HasValue) + { + conditions2.Append($"AND {columns.Timestamp} <= @EndDate "); + } + + if (conditions2.Length <= 0) + { + return; + } + + queryBuilder + .Append("WHERE TRUE ") + .Append(conditions2); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj b/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj index 209595e7..85f3e8ac 100644 --- a/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj +++ b/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj @@ -4,7 +4,7 @@ Serilog.UI.MySqlProvider netstandard2.0 latest - 3.0.0 + 3.1.0 MySQL and MariaDB data provider for Serilog UI. serilog serilog-ui serilog.sinks.mysql serilog.sinks.mariadb @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs b/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs index 5a603b03..24c8ccb8 100644 --- a/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs +++ b/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs @@ -1,29 +1,20 @@ -using System; +using Dapper; +using MySqlConnector; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MySqlProvider.Extensions; +using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Dapper; -using MySqlConnector; -using Serilog.Ui.Core; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; namespace Serilog.Ui.MySqlProvider.Shared; -public abstract class DataProvider(RelationalDbOptions options) : IDataProvider +public abstract class DataProvider(MySqlDbOptions options, MySqlQueryBuilder queryBuilder) : IDataProvider where T : MySqlLogModel { - protected virtual string ColumnTimestampName => "TimeStamp"; - - protected virtual string ColumnLevelName => "Level"; - - protected virtual string ColumnMessageName => "Message"; - - protected readonly RelationalDbOptions Options = options ?? throw new ArgumentNullException(nameof(options)); + public abstract string Name { get; } public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) { @@ -31,30 +22,19 @@ public abstract class DataProvider(RelationalDbOptions options) : IDataProvid var logsTask = GetLogsAsync(queryParams); var logCountTask = CountLogsAsync(queryParams); - - await Task.WhenAll(logsTask, logCountTask); + await Task.WhenAll(logsTask); return (await logsTask, await logCountTask); } - public abstract string Name { get; } - - protected virtual string SelectQuery => "SELECT * "; - private async Task> GetLogsAsync(FetchLogsQuery queryParams) { - var queryBuilder = new StringBuilder(); - queryBuilder.Append(SelectQuery).Append($"FROM `{Options.TableName}` "); - - GenerateWhereClause(queryBuilder, queryParams); - var sortClause = GenerateSortClause(queryParams.SortOn, queryParams.SortBy); + string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); + int rowNoStart = queryParams.Page * queryParams.Count; - queryBuilder.Append($"ORDER BY {sortClause} LIMIT @Offset, @Count"); + using MySqlConnection connection = new(options.ConnectionString); - var rowNoStart = queryParams.Page * queryParams.Count; - - using var connection = new MySqlConnection(Options.ConnectionString); - var param = new + IEnumerable logs = await connection.QueryAsync(query, new { Offset = rowNoStart, queryParams.Count, @@ -62,18 +42,15 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, queryParams.StartDate, queryParams.EndDate - }; - - var logs = await connection.QueryAsync(queryBuilder.ToString(), param); + }); return logs .Select((item, i) => { item.SetRowNo(rowNoStart, i); item.Level ??= item.LogLevel; - // both sinks save UTC but MariaDb is queried as Unspecified, MySql is queried as Local - var ts = DateTime.SpecifyKind(item.Timestamp, - item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind); + // both sinks save UTC but MariaDb is queried as Unspecified, MySql is queried as Local + var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind); item.Timestamp = ts.ToUniversalTime(); return item; }) @@ -82,13 +59,11 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam private async Task CountLogsAsync(FetchLogsQuery queryParams) { - var queryBuilder = new StringBuilder(); - queryBuilder.Append($"SELECT COUNT(Id) FROM `{Options.TableName}` "); + string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); - GenerateWhereClause(queryBuilder, queryParams); + using MySqlConnection connection = new(options.ConnectionString); - using var connection = new MySqlConnection(Options.ConnectionString); - return await connection.ExecuteScalarAsync(queryBuilder.ToString(), + return await connection.ExecuteScalarAsync(query, new { queryParams.Level, @@ -97,50 +72,4 @@ private async Task CountLogsAsync(FetchLogsQuery queryParams) queryParams.EndDate }); } - - /// - /// If Exception property is flagged with , - /// it removes the Where query part on the Exception field. - /// - /// - protected virtual string SearchCriteriaWhereQuery() - { - var exceptionProperty = typeof(T).GetProperty(nameof(MySqlLogModel.Exception)); - var att = exceptionProperty?.GetCustomAttribute(); - return att is null ? "OR Exception LIKE @Search" : string.Empty; - } - - private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams) - { - var conditionStart = "WHERE"; - - if (!string.IsNullOrWhiteSpace(queryParams.Level)) - { - queryBuilder.Append($"WHERE {ColumnLevelName} = @Level "); - conditionStart = "AND"; - } - - if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria)) - { - queryBuilder.Append($"{conditionStart} ({ColumnMessageName} LIKE @Search {SearchCriteriaWhereQuery()}) "); - conditionStart = "AND"; - } - - if (queryParams.StartDate != null) - { - queryBuilder.Append($"{conditionStart} {ColumnTimestampName} >= @StartDate "); - conditionStart = "AND"; - } - - if (queryParams.EndDate != null) - { - queryBuilder.Append($"{conditionStart} {ColumnTimestampName} <= @EndDate "); - } - } - - private string GenerateSortClause(SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) - { - var sortProperty = sortOn == SearchOptions.SortProperty.Level ? ColumnLevelName : sortOn.ToString(); - return $"{sortProperty} {sortBy.ToString().ToUpper()}"; - } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs index 06cea7c4..0ff48ee9 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs @@ -1,4 +1,5 @@ using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; using Serilog.Ui.PostgreSqlProvider.Models; namespace Serilog.Ui.PostgreSqlProvider.Extensions; @@ -9,10 +10,9 @@ public class PostgreSqlDbOptions : RelationalDbOptions /// public PostgreSqlDbOptions(string defaultSchemaName) : base(defaultSchemaName) { + ColumnNames = new PostgreSqlAlternativeSinkColumnNames(); } - internal SinkColumnNames ColumnNames = new PostgreSqlAlternativeSinkColumnNames(); - /// /// It gets or sets SinkType. /// The sink that used to store logs in the PostgreSQL database. This data provider supports @@ -31,6 +31,9 @@ public PostgreSqlDbOptions WithSinkType(PostgreSqlSinkType sinkType) ColumnNames = sinkType == PostgreSqlSinkType.SerilogSinksPostgreSQLAlternative ? new PostgreSqlAlternativeSinkColumnNames() : new PostgreSqlSinkColumnNames(); + return this; } + + internal SinkColumnNames ColumnNames { get; set; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs index 9f217bcb..8ba23502 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -1,56 +1,51 @@ -using System; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Serilog.Ui.Core; using Serilog.Ui.Core.Interfaces; using Serilog.Ui.PostgreSqlProvider.Models; +using System; -namespace Serilog.Ui.PostgreSqlProvider.Extensions +namespace Serilog.Ui.PostgreSqlProvider.Extensions; + +/// +/// PostgreSQL data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions { /// - /// PostgreSQL data provider specific extension methods for . + /// Configures the SerilogUi to connect to a PostgreSQL database. /// - public static class SerilogUiOptionBuilderExtensions - { - /// - /// Configures the SerilogUi to connect to a PostgreSQL database. - /// - /// The Serilog UI option builder. - /// The Postgres Sql options action. - public static ISerilogUiOptionsBuilder UseNpgSql( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) => optionsBuilder.UseNpgSql(setupOptions); - - /// - /// Configures the SerilogUi to connect to a PostgreSQL database. - /// - /// The log model, containing any additional columns. It must inherit . - /// The Serilog UI option builder. - /// The Postgres Sql options action. - public static ISerilogUiOptionsBuilder UseNpgSql( - this ISerilogUiOptionsBuilder optionsBuilder, - Action setupOptions - ) where T : PostgresLogModel - { - var dbOptions = new PostgreSqlDbOptions("public"); - setupOptions(dbOptions); - dbOptions.Validate(); + /// The Serilog UI option builder. + /// The Postgres Sql options action. + public static ISerilogUiOptionsBuilder UseNpgSql(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + => optionsBuilder.UseNpgSql(setupOptions); - var providerName = dbOptions.GetProviderName(PostgresDataProvider.ProviderName); - - optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - - var customModel = typeof(T) != typeof(PostgresLogModel); - if (customModel) - { - optionsBuilder.RegisterColumnsInfo(providerName); - optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions)); + /// + /// Configures the SerilogUi to connect to a PostgreSQL database. + /// + /// The log model, containing any additional columns. It must inherit . + /// The Serilog UI option builder. + /// The Postgres Sql options action. + public static ISerilogUiOptionsBuilder UseNpgSql(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions) + where T : PostgresLogModel + { + PostgreSqlDbOptions dbOptions = new("public"); + setupOptions(dbOptions); + dbOptions.Validate(); - return optionsBuilder; - } + string providerName = dbOptions.GetProviderName(PostgresDataProvider.ProviderName); + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); - optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions)); - return optionsBuilder; + bool customModel = typeof(T) != typeof(PostgresLogModel); + if (customModel) + { + optionsBuilder.RegisterColumnsInfo(providerName); + optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); } + else + { + optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder())); + } + + return optionsBuilder; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs index d0149d7f..546204d3 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs @@ -17,23 +17,23 @@ public class PostgresLogModel : LogModel private string _level = string.Empty; /// - public sealed override int RowNo => base.RowNo; + public override sealed int RowNo => base.RowNo; /// - public sealed override string? Message { get; set; } + public override sealed string? Message { get; set; } /// - public sealed override DateTime Timestamp { get; set; } + public override sealed DateTime Timestamp { get; set; } /// - public sealed override string? Level + public override sealed string? Level { get => _level; set => _level = LogLevelConverter.GetLevelName(value); } /// - /// It get or sets LogEventSerialized. + /// It gets or sets LogEventSerialized. /// [JsonIgnore] public string LogEvent { get; set; } = string.Empty; diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs index 8f72b6e4..b1ecae3c 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs @@ -1,4 +1,6 @@ -namespace Serilog.Ui.PostgreSqlProvider.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.PostgreSqlProvider.Models; internal class PostgreSqlAlternativeSinkColumnNames : SinkColumnNames { @@ -7,8 +9,8 @@ public PostgreSqlAlternativeSinkColumnNames() Exception = "Exception"; Level = "Level"; LogEventSerialized = "LogEvent"; + Message = "Message"; MessageTemplate = "MessageTemplate"; - RenderedMessage = "Message"; Timestamp = "Timestamp"; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs index a2d56427..01153b9d 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs @@ -1,14 +1,16 @@ -namespace Serilog.Ui.PostgreSqlProvider.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.PostgreSqlProvider.Models; internal class PostgreSqlSinkColumnNames : SinkColumnNames { public PostgreSqlSinkColumnNames() { - RenderedMessage = "message"; - MessageTemplate = "message_template"; - Level = "level"; - Timestamp = "timestamp"; Exception = "exception"; + Level = "level"; LogEventSerialized = "log_event"; + Message = "message"; + MessageTemplate = "message_template"; + Timestamp = "timestamp"; } } \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs deleted file mode 100644 index 296c5d29..00000000 --- a/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Serilog.Ui.PostgreSqlProvider.Models; - -internal abstract class SinkColumnNames -{ - public string RenderedMessage { get; set; } = string.Empty; - - public string MessageTemplate { get; set; } = string.Empty; - - public string Level { get; set; } = string.Empty; - - public string Timestamp { get; set; } = string.Empty; - - public string Exception { get; set; } = string.Empty; - - public string LogEventSerialized { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs similarity index 67% rename from src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs rename to src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs index fc76c115..3a2d44be 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs @@ -1,38 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; +using Dapper; using Npgsql; using Serilog.Ui.Core; using Serilog.Ui.Core.Models; using Serilog.Ui.PostgreSqlProvider.Extensions; using Serilog.Ui.PostgreSqlProvider.Models; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Serilog.Ui.PostgreSqlProvider; /// -public class PostgresDataProvider(PostgreSqlDbOptions options) : PostgresDataProvider(options); +public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) + : PostgresDataProvider(options, queryBuilder); /// -public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvider +public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) : IDataProvider where T : PostgresLogModel { internal const string ProviderName = "NPGSQL"; - private readonly PostgreSqlDbOptions _options = options ?? throw new ArgumentNullException(nameof(options)); - /// - public string Name => _options.GetProviderName(ProviderName); + public string Name => options.GetProviderName(ProviderName); /// public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) { queryParams.ToUtcDates(); - var logsTask = GetLogsAsync(queryParams); - var logCountTask = CountLogsAsync(queryParams); + Task> logsTask = GetLogsAsync(queryParams); + Task logCountTask = CountLogsAsync(queryParams); await Task.WhenAll(logsTask, logCountTask); return (await logsTask, await logCountTask); @@ -40,12 +38,12 @@ public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvide private async Task> GetLogsAsync(FetchLogsQuery queryParams) { - var query = options.ColumnNames.BuildFetchLogsQuery(_options.Schema, _options.TableName, queryParams); - var rowNoStart = queryParams.Page * queryParams.Count; + string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); + int rowNoStart = queryParams.Page * queryParams.Count; - await using var connection = new NpgsqlConnection(_options.ConnectionString); + await using NpgsqlConnection connection = new(options.ConnectionString); - var logs = await connection.QueryAsync(query, + IEnumerable logs = await connection.QueryAsync(query, new { Offset = rowNoStart, @@ -68,9 +66,9 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam private async Task CountLogsAsync(FetchLogsQuery queryParams) { - var query = options.ColumnNames.BuildCountLogsQuery(_options.Schema, _options.TableName, queryParams); + string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams); - await using var connection = new NpgsqlConnection(_options.ConnectionString); + await using NpgsqlConnection connection = new(options.ConnectionString); return await connection.ExecuteScalarAsync(query, new diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs new file mode 100644 index 00000000..675fe49a --- /dev/null +++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs @@ -0,0 +1,122 @@ +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.PostgreSqlProvider.Models; +using System; +using System.Text; +using static Serilog.Ui.Core.Models.SearchOptions; + +namespace Serilog.Ui.PostgreSqlProvider; + +/// +/// Provides methods to build SQL queries specifically for PostgreSQL to fetch and count logs. +/// +/// The type of the log model. +public class PostgresQueryBuilder : SqlQueryBuilder where TModel : LogModel +{ + /// + public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + GenerateSelectClause(queryStr, columns, schema, tableName); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Count OFFSET @Offset"); + + return queryStr.ToString(); + } + + /// + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append($"SELECT COUNT(\"{columns.Message}\") ") + .Append($"FROM \"{schema}\".\"{tableName}\""); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + return queryStr.ToString(); + } + + /// + protected override string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy) + => $"ORDER BY \"{GetSortColumnName(columns, sortOn)}\" {sortBy.ToString().ToUpper()}"; + + /// + /// Generates the SELECT clause for the SQL query. + /// + /// The StringBuilder to append the SELECT clause to. + /// The column names used in the sink for logging. + /// The schema of the table. + /// The name of the table. + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + { + if (typeof(TModel) != typeof(PostgresLogModel)) + { + queryBuilder.Append("SELECT *"); + } + else + { + queryBuilder.Append($"SELECT \"{columns.Message}\", ") + .Append($"\"{columns.MessageTemplate}\", ") + .Append($"\"{columns.Level}\", ") + .Append($"\"{columns.Timestamp}\", ") + .Append($"\"{columns.Exception}\", ") + .Append($"\"{columns.LogEventSerialized}\" AS \"Properties\""); + } + + queryBuilder.Append($" FROM \"{schema}\".\"{tableName}\" "); + } + + /// + /// Generates the WHERE clause for the SQL query. + /// + /// The StringBuilder to append the WHERE clause to. + /// The column names used in the sink for logging. + /// The log level to filter by. + /// The search criteria to filter by. + /// The start date to filter by. + /// The end date to filter by. + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + StringBuilder conditions = new(); + + if (!string.IsNullOrWhiteSpace(level)) + { + conditions.Append($"AND \"{columns.Level}\" = @Level "); + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + conditions.Append($"AND (\"{columns.Message}\" LIKE @Search "); + conditions.Append(AddExceptionToWhereClause() ? $"OR \"{columns.Exception}\" LIKE @Search) " : ") "); + } + + if (startDate.HasValue) + { + conditions.Append($"AND \"{columns.Timestamp}\" >= @StartDate "); + } + + if (endDate.HasValue) + { + conditions.Append($"AND \"{columns.Timestamp}\" <= @EndDate "); + } + + if (conditions.Length <= 0) + { + return; + } + + queryBuilder + .Append("WHERE TRUE ") + .Append(conditions); + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs deleted file mode 100644 index 49d8171a..00000000 --- a/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; -using Serilog.Ui.Core.Attributes; -using Serilog.Ui.Core.Models; -using Serilog.Ui.PostgreSqlProvider.Models; -using static Serilog.Ui.Core.Models.SearchOptions; - -namespace Serilog.Ui.PostgreSqlProvider; - -internal static class QueryBuilder -{ - internal static string BuildFetchLogsQuery(this SinkColumnNames _columns, string schema, string tableName, FetchLogsQuery query) - where T : PostgresLogModel - { - var sortClause = _columns.GenerateSortClause(query.SortOn, query.SortBy); - - return new StringBuilder() - .GenerateSelectClause(_columns) - .Append($" FROM \"{schema}\".\"{tableName}\"") - .GenerateWhereClause(_columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate) - .Append($" ORDER BY {sortClause} LIMIT @Count OFFSET @Offset") - .ToString(); - } - - internal static string BuildCountLogsQuery(this SinkColumnNames _columns, string schema, string tableName, FetchLogsQuery query) - where T : PostgresLogModel - { - return new StringBuilder() - .Append($"SELECT COUNT(\"{_columns.RenderedMessage}\") ") - .Append($"FROM \"{schema}\".\"{tableName}\"") - .GenerateWhereClause(_columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate) - .ToString(); - } - - private static StringBuilder GenerateSelectClause(this StringBuilder queryBuilder, SinkColumnNames _columns) - where T : PostgresLogModel - { - if (typeof(T) != typeof(PostgresLogModel)) - { - return queryBuilder.Append("SELECT *"); - } - - return queryBuilder.Append($"SELECT \"{_columns.RenderedMessage}\", ") - .Append($"\"{_columns.MessageTemplate}\", ") - .Append($"\"{_columns.Level}\", ") - .Append($"\"{_columns.Timestamp}\", ") - .Append($"\"{_columns.Exception}\", ") - .Append($"\"{_columns.LogEventSerialized}\" AS \"Properties\""); - } - - private static StringBuilder GenerateWhereClause(this StringBuilder queryBuilder, - SinkColumnNames _columns, - string? level, - string? searchCriteria, - DateTime? startDate, - DateTime? endDate) - where T : PostgresLogModel - { - var conditions = new List(); - - if (!string.IsNullOrWhiteSpace(level)) - { - conditions.Add($"\"{_columns.Level}\" = @Level"); - } - - if (!string.IsNullOrWhiteSpace(searchCriteria)) - { - var exceptionCondition = AddExceptionToWhereClause() ? $"OR \"{_columns.Exception}\" LIKE @Search" : string.Empty; - conditions.Add($"(\"{_columns.RenderedMessage}\" LIKE @Search {exceptionCondition})"); - } - - if (startDate.HasValue) - { - conditions.Add($"\"{_columns.Timestamp}\" >= @StartDate"); - } - - if (endDate.HasValue) - { - conditions.Add($"\"{_columns.Timestamp}\" <= @EndDate"); - } - - if (conditions.Count <= 0) return queryBuilder; - - return queryBuilder - .Append(" WHERE TRUE AND ") - .Append(string.Join(" AND ", conditions)); - } - - private static bool AddExceptionToWhereClause() - where T : PostgresLogModel - { - var exceptionProperty = typeof(T).GetProperty(nameof(PostgresLogModel.Exception)); - var att = exceptionProperty?.GetCustomAttribute(); - return att is null; - } - - private static string GenerateSortClause(this SinkColumnNames _columns, SortProperty sortOn, SortDirection sortBy) - { - var sortPropertyName = sortOn switch - { - SortProperty.Timestamp => _columns.Timestamp, - SortProperty.Level => _columns.Level, - SortProperty.Message => _columns.RenderedMessage, - _ => _columns.Timestamp, - }; - - return $"\"{sortPropertyName}\" {sortBy.ToString().ToUpper()}"; - } -} \ No newline at end of file diff --git a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj index 9aaffd0f..47a97a7a 100644 --- a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj +++ b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj @@ -4,10 +4,8 @@ Serilog.UI.PostgreSqlProvider netstandard2.0 latest - 3.0.0 - + 3.1.0 True - PostgreSQL data provider for Serilog UI. serilog serilog-ui serilog.sinks.postgresql postgresql diff --git a/src/Serilog.Ui.RavenDbProvider/Serilog.Ui.RavenDbProvider.csproj b/src/Serilog.Ui.RavenDbProvider/Serilog.Ui.RavenDbProvider.csproj index 9d938aab..a055f756 100644 --- a/src/Serilog.Ui.RavenDbProvider/Serilog.Ui.RavenDbProvider.csproj +++ b/src/Serilog.Ui.RavenDbProvider/Serilog.Ui.RavenDbProvider.csproj @@ -4,7 +4,7 @@ Serilog.Ui.RavenDbProvider netstandard2.0 latest - 2.0.0 + 2.1.0 enable enable diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs new file mode 100644 index 00000000..906d551a --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SerilogUiOptionBuilderExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Interfaces; +using Serilog.Ui.Core.Models.Options; +using System; + +namespace Serilog.Ui.SqliteDataProvider.Extensions; + +/// +/// SQLite data provider specific extension methods for . +/// +public static class SerilogUiOptionBuilderExtensions +{ + /// Configures the SerilogUi to connect to a SQLite database. + /// The options builder. + /// The SQLite options action. + public static ISerilogUiOptionsBuilder UseSqliteServer( + this ISerilogUiOptionsBuilder optionsBuilder, + Action setupOptions) + { + var dbOptions = new SqliteDbOptions(); + setupOptions(dbOptions); + dbOptions.Validate(); + + string providerName = dbOptions.GetProviderName(SqliteDataProvider.SqliteProviderName); + optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName); + optionsBuilder.Services.AddScoped(_ => new SqliteDataProvider(dbOptions, new SqliteQueryBuilder())); + + return optionsBuilder; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs b/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs new file mode 100644 index 00000000..449a55b2 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Extensions/SqliteDbOptions.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.Core.QueryBuilder.Sql; +using Serilog.Ui.SqliteDataProvider.Models; + +namespace Serilog.Ui.SqliteDataProvider.Extensions; + +public class SqliteDbOptions() : RelationalDbOptions("ununsed") +{ + public SinkColumnNames ColumnNames { get; } = new SqliteSinkColumnNames(); +} diff --git a/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs b/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs new file mode 100644 index 00000000..a86e515e --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Models/SqliteSinkColumnNames.cs @@ -0,0 +1,16 @@ +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.SqliteDataProvider.Models; + +internal class SqliteSinkColumnNames : SinkColumnNames +{ + public SqliteSinkColumnNames() + { + Exception = "Exception"; + Level = "Level"; + LogEventSerialized = "Properties"; + Message = "RenderedMessage"; + MessageTemplate = ""; + Timestamp = "Timestamp"; + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj new file mode 100644 index 00000000..9b25cc3e --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/Serilog.Ui.SqliteDataProvider.csproj @@ -0,0 +1,24 @@ + + + + Serilog.UI.SqliteProvider + netstandard2.0 + latest + 1.0.0 + + Tech Garage (team) + SQLite data provider for Serilog UI. + serilog serilog-ui serilog.sinks.sqlite sqlite + + + + + + + + + + + + + diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs new file mode 100644 index 00000000..c9bc3584 --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteDataProvider.cs @@ -0,0 +1,82 @@ +using Ardalis.GuardClauses; +using Dapper; +using Microsoft.Data.Sqlite; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Models; +using Serilog.Ui.SqliteDataProvider.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Serilog.Ui.SqliteDataProvider; + +public class SqliteDataProvider(SqliteDbOptions options, SqliteQueryBuilder queryBuilder) : IDataProvider +{ + internal const string SqliteProviderName = "SQLite"; + private readonly SqliteDbOptions _options = Guard.Against.Null(options); + + public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default) + { + queryParams.ToUtcDates(); // assuming data is saved in UTC, due to UTC predictability + + var logsTask = GetLogsAsync(queryParams); + var logCountTask = CountLogsAsync(queryParams); + + await Task.WhenAll(logsTask, logCountTask); + + return (await logsTask, await logCountTask); + } + + public string Name => _options.GetProviderName(SqliteProviderName); + + private async Task> GetLogsAsync(FetchLogsQuery queryParams) + { + var query = queryBuilder.BuildFetchLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); + + var rowNoStart = queryParams.Page * queryParams.Count; + + using var connection = new SqliteConnection(_options.ConnectionString); + var queryParameters = new + { + Offset = rowNoStart, + queryParams.Count, + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + StartDate = StringifyDate(queryParams.StartDate), + EndDate = StringifyDate(queryParams.EndDate) + }; + var logs = await connection.QueryAsync(query.ToString(), queryParameters); + + return logs.Select((item, i) => + { + item.PropertyType = "json"; + + var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind); + item.Timestamp = ts.ToUniversalTime(); + + item.SetRowNo(rowNoStart, i); + return item; + }).ToList(); + } + + private Task CountLogsAsync(FetchLogsQuery queryParams) + { + var query = queryBuilder.BuildCountLogsQuery(_options.ColumnNames, _options.Schema, _options.TableName, queryParams); + + using var connection = new SqliteConnection(_options.ConnectionString); + + return connection.QueryFirstOrDefaultAsync( + query.ToString(), + new + { + queryParams.Level, + Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null, + StartDate = StringifyDate(queryParams.StartDate), + EndDate = StringifyDate(queryParams.EndDate) + }); + } + + private static string StringifyDate(DateTime? date) => date.HasValue ? date.Value.ToString("s") + ".999" : "null"; +} diff --git a/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs new file mode 100644 index 00000000..54f7bf2c --- /dev/null +++ b/src/Serilog.Ui.SqliteDataProvider/SqliteQueryBuilder.cs @@ -0,0 +1,84 @@ +using System; +using System.Text; +using Serilog.Ui.Core.Models; +using Serilog.Ui.Core.QueryBuilder.Sql; + +namespace Serilog.Ui.SqliteDataProvider; + +/// +/// Provides methods to build SQL queries specifically for Sqlite to fetch and count logs. +/// +/// The type of the log model. +public class SqliteQueryBuilder : SqlQueryBuilder +{ + /// + public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + GenerateSelectClause(queryStr, columns, schema, tableName); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Offset, @Count"); + + return queryStr.ToString(); + } + + /// + public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query) + { + StringBuilder queryStr = new(); + + queryStr.Append($"SELECT COUNT(Id) FROM {tableName} "); + + GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate); + + return queryStr.ToString(); + } + + protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy) + => $"ORDER BY {GetSortColumnName(columns, sortOn)} {sortBy.ToString().ToUpper()}"; + + /// + private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName) + { + queryBuilder.Append($"SELECT Id, {columns.Message} AS Message, {columns.Level}, {columns.Timestamp}, {columns.Exception}, {columns.LogEventSerialized} "); + queryBuilder.Append($"FROM {tableName} "); + } + + /// + private static void GenerateWhereClause( + StringBuilder queryBuilder, + SinkColumnNames columns, + string? level, + string? searchCriteria, + DateTime? startDate, + DateTime? endDate) + { + var conditionStart = "WHERE"; + + if (!string.IsNullOrWhiteSpace(level)) + { + queryBuilder.Append($"{conditionStart} {columns.Level} = @Level "); + conditionStart = "AND"; + } + + if (!string.IsNullOrWhiteSpace(searchCriteria)) + { + queryBuilder.Append($"{conditionStart} ({columns.Message} LIKE @Search OR {columns.Exception} LIKE @Search) "); + conditionStart = "AND"; + } + + if (startDate != null) + { + queryBuilder.Append($"{conditionStart} {columns.Timestamp} >= @StartDate "); + conditionStart = "AND"; + } + + if (endDate != null) + { + queryBuilder.Append($"{conditionStart} {columns.Timestamp} <= @EndDate "); + } + } +} \ No newline at end of file diff --git a/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs b/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs index 03b6546b..a5b07e5a 100644 --- a/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs +++ b/src/Serilog.Ui.Web/Extensions/ServiceCollectionExtensions.cs @@ -40,12 +40,17 @@ public static IServiceCollection AddSerilogUi(this IServiceCollection services, services.AddScoped(); services.AddSingleton(); - services.AddScoped(); - services.Decorate(); - - services.AddScoped(); - services.Decorate(); - + services.AddScoped(); + services.AddScoped(sp => new SerilogUiEndpointsDecorator( + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(); + services.AddScoped(sp => new SerilogUiAppRoutesDecorator( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + services.AddScoped(); return services; diff --git a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj index a8905db6..02173b62 100644 --- a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj +++ b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj @@ -4,18 +4,11 @@ Serilog.UI net6.0;net7.0;net8.0 latest - 3.0.2 + 3.1.0 - - - - - - - diff --git a/src/Serilog.Ui.Web/package.json b/src/Serilog.Ui.Web/package.json index 6ae1c9f5..d4d52386 100644 --- a/src/Serilog.Ui.Web/package.json +++ b/src/Serilog.Ui.Web/package.json @@ -1,5 +1,5 @@ { - "version": "3.0.2", + "version": "3.1.0", "name": "serilog-ui", "private": true, "type": "module", @@ -15,14 +15,14 @@ }, "dependencies": { "@fontsource/mononoki": "^5.1.0", - "@mantine/core": "^7.12.2", - "@mantine/dates": "^7.12.2", - "@mantine/hooks": "^7.12.2", - "@mantine/notifications": "^7.12.2", - "@tabler/icons-react": "^3.17.0", - "@tanstack/react-query": "^5.56.2", + "@mantine/core": "^7.13.2", + "@mantine/dates": "^7.13.2", + "@mantine/hooks": "^7.13.2", + "@mantine/notifications": "^7.13.2", + "@tabler/icons-react": "^3.19.0", + "@tanstack/react-query": "^5.59.8", "dayjs": "^1.11.13", - "jose": "^5.9.2", + "jose": "^5.9.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", @@ -30,26 +30,26 @@ "xml-formatter": "^3.6.3" }, "devDependencies": { - "@faker-js/faker": "^9.0.1", + "@faker-js/faker": "^9.0.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.5.5", - "@types/react": "^18.3.8", + "@types/node": "^22.7.5", + "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react-swc": "^3.7.0", - "@vitest/coverage-istanbul": "^2.1.1", - "@vitest/ui": "^2.1.1", + "@vitejs/plugin-react-swc": "^3.7.1", + "@vitest/coverage-istanbul": "^2.1.2", + "@vitest/ui": "^2.1.2", "@welldone-software/why-did-you-render": "^8.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-html": "^8.1.1", - "eslint-plugin-import": "^2.30.0", + "eslint-plugin-html": "^8.1.2", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^7.1.0", - "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react": "^7.37.1", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-testing-library": "^6.3.0", "eslint-plugin-vitest": "^0.5.4", @@ -61,15 +61,15 @@ "postcss-simple-vars": "^7.0.1", "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^4.1.0", - "shiki": "^1.18.0", + "shiki": "^1.22.0", "testing-library-selector": "^0.3.1", - "typescript": "^5.6.2", - "typescript-eslint": "^8.6.0", - "vite": "^5.4.7", + "typescript": "^5.6.3", + "typescript-eslint": "^8.8.1", + "vite": "^5.4.8", "vite-plugin-checker": "^0.8.0", "vite-plugin-mkcert": "^1.17.6", "vite-tsconfig-paths": "^5.0.1", - "vitest": "^2.1.1", + "vitest": "^2.1.2", "vitest-sonar-reporter": "^2.0.0" }, "engines": { diff --git a/src/Serilog.Ui.Web/yarn.lock b/src/Serilog.Ui.Web/yarn.lock index 91870f78..61de84d5 100644 --- a/src/Serilog.Ui.Web/yarn.lock +++ b/src/Serilog.Ui.Web/yarn.lock @@ -376,10 +376,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@faker-js/faker@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.0.1.tgz#5e201ffc4524d00a200c648d2be55be6e25b3c3e" - integrity sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA== +"@faker-js/faker@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.0.3.tgz#be817db896b07d1716bc65d9aad1ba587b499826" + integrity sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA== "@floating-ui/core@^1.0.0": version "1.6.2" @@ -532,10 +532,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mantine/core@^7.12.2": - version "7.12.2" - resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.12.2.tgz#d8fb7969a62dd5367331b141e053c4b2989c677a" - integrity sha512-FrMHOKq4s3CiPIxqZ9xnVX7H4PEGNmbtHMvWO/0YlfPgoV0Er/N/DNJOFW1ys4WSnidPTayYeB41riyxxGOpRQ== +"@mantine/core@^7.13.2": + version "7.13.2" + resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.13.2.tgz#8fb8ee2857a40f237ffed370a6ad768fbb1b3be2" + integrity sha512-nD8oKIal+KdthqF074+ZG21035QBEAKso2zK9D6zWxRTLVCjLCNSc5JSXrXLrdBTnvPQGY26yunX4+MEPlmrHg== dependencies: "@floating-ui/react" "^0.26.9" clsx "^2.1.1" @@ -544,30 +544,30 @@ react-textarea-autosize "8.5.3" type-fest "^4.12.0" -"@mantine/dates@^7.12.2": - version "7.12.2" - resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-7.12.2.tgz#32fd7341283450487de884b6f996647431690938" - integrity sha512-qsDDl9qF80QLG1n6JiysyELAhbNLbV3qmXRAIU3GJLLxtZfyD9ntOUg0B64EpNl3Py4btXNo4yniFdu1JSUgwg== +"@mantine/dates@^7.13.2": + version "7.13.2" + resolved "https://registry.yarnpkg.com/@mantine/dates/-/dates-7.13.2.tgz#b6e2a83f507efe25b211f1d8765dd9af7a827c7d" + integrity sha512-FSLGTM5s47mmHnIudRxrMXjE1EO56Qp01nATa9OwqVgVYVxxJ5xvS1ys5yxSGSE1jQk+3kyYQXHyLFcqbFhIVA== dependencies: clsx "^2.1.1" -"@mantine/hooks@^7.12.2": - version "7.12.2" - resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.12.2.tgz#f8e6a8345bb0892d8d1f5d1dc544a568572b79f4" - integrity sha512-dVMw8jpM0hAzc8e7/GNvzkk9N0RN/m+PKycETB3H6lJGuXJJSRR4wzzgQKpEhHwPccktDpvb4rkukKDq2jA8Fg== +"@mantine/hooks@^7.13.2": + version "7.13.2" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.13.2.tgz#ec6a17faee4c8f3c0fe06f7b66c0e4a12c9b1ad1" + integrity sha512-NKfGl2sKZw6zF//AfAFJrVDftjg7DKCn0h8rwJBIZCKi9axhwlV0Mvlqe2dep8QuM7O/uLLJSymSKIv1gaxIJg== -"@mantine/notifications@^7.12.2": - version "7.12.2" - resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.12.2.tgz#531a6a3f23adc579b0a8f322ccf42b1789327698" - integrity sha512-gTvLHkoAZ42v5bZxibP9A50djp5ndEwumVhHSa7mxQ8oSS23tt3It/6hOqH7M+9kHY0a8s+viMiflUzTByA9qg== +"@mantine/notifications@^7.13.2": + version "7.13.2" + resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-7.13.2.tgz#0be074470b0a91a6dc7a6834e67c301d7ebea903" + integrity sha512-14vFJtR0wjO8Won96UMLxIGmKetR0ocBxcghtuGh6+wnXt6r/ezfQKsdGkkNj6w91I+0Nl9jspcxEekE5q2tBQ== dependencies: - "@mantine/store" "7.12.2" + "@mantine/store" "7.13.2" react-transition-group "4.4.5" -"@mantine/store@7.12.2": - version "7.12.2" - resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.12.2.tgz#da576a63d860d44525bd18c9dd3977b6d2840011" - integrity sha512-NqL31sO/KcAETEWP/CiXrQOQNoE4168vZsxyXacQHGBueVMJa64WIDQtKLHrCnFRMws3vsXF02/OO4bH4XGcMQ== +"@mantine/store@7.13.2": + version "7.13.2" + resolved "https://registry.yarnpkg.com/@mantine/store/-/store-7.13.2.tgz#aba23573bff42eda28716f422275929afde086c9" + integrity sha512-JcBGOqRynYiRWzw1rYdmViB/lfeYSec2EXVdSt4eJv+RPICsjjuqrIc3sNzfqJEGxcN4hGSlaeBriSh05K+vNQ== "@mswjs/interceptors@^0.35.8": version "0.35.8" @@ -735,237 +735,237 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273" integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA== -"@rollup/rollup-android-arm-eabi@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz#155c7d82c1b36c3ad84d9adf9b3cd520cba81a0f" - integrity sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg== - -"@rollup/rollup-android-arm64@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz#b94b6fa002bd94a9cbd8f9e47e23b25e5bd113ba" - integrity sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g== - -"@rollup/rollup-darwin-arm64@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz#0934126cf9cbeadfe0eb7471ab5d1517e8cd8dcc" - integrity sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ== - -"@rollup/rollup-darwin-x64@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz#0ce8e1e0f349778938c7c90e4bdc730640e0a13e" - integrity sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA== - -"@rollup/rollup-linux-arm-gnueabihf@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz#5669d34775ad5d71e4f29ade99d0ff4df523afb6" - integrity sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g== - -"@rollup/rollup-linux-arm-musleabihf@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz#f6d1a0e1da4061370cb2f4244fbdd727c806dd88" - integrity sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA== - -"@rollup/rollup-linux-arm64-gnu@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz#ed96a05e99743dee4d23cc4913fc6e01a0089c88" - integrity sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw== - -"@rollup/rollup-linux-arm64-musl@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz#057ea26eaa7e537a06ded617d23d57eab3cecb58" - integrity sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ== - -"@rollup/rollup-linux-powerpc64le-gnu@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz#6e6e1f9404c9bf3fbd7d51cd11cd288a9a2843aa" - integrity sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw== - -"@rollup/rollup-linux-riscv64-gnu@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz#eef1536a53f6e6658a2a778130e6b1a4a41cb439" - integrity sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ== - -"@rollup/rollup-linux-s390x-gnu@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz#2b28fb89ca084efaf8086f435025d96b4a966957" - integrity sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg== - -"@rollup/rollup-linux-x64-gnu@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz#5226cde6c6b495b04a3392c1d2c572844e42f06b" - integrity sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g== - -"@rollup/rollup-linux-x64-musl@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz#2c2412982e6c2a00a2ecac6d548ebb02f0aa6ca4" - integrity sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg== - -"@rollup/rollup-win32-arm64-msvc@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz#fbb6ef5379199e2ec0103ef32877b0985c773a55" - integrity sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q== - -"@rollup/rollup-win32-ia32-msvc@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz#d50e2082e147e24d87fe34abbf6246525ec3845a" - integrity sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA== - -"@rollup/rollup-win32-x64-msvc@4.21.3": - version "4.21.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz#4115233aa1bd5a2060214f96d8511f6247093212" - integrity sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA== +"@rollup/rollup-android-arm-eabi@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz#8b613b9725e8f9479d142970b106b6ae878610d5" + integrity sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w== + +"@rollup/rollup-android-arm64@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz#654ca1049189132ff602bfcf8df14c18da1f15fb" + integrity sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA== + +"@rollup/rollup-darwin-arm64@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz#6d241d099d1518ef0c2205d96b3fa52e0fe1954b" + integrity sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q== + +"@rollup/rollup-darwin-x64@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz#42bd19d292a57ee11734c980c4650de26b457791" + integrity sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw== + +"@rollup/rollup-linux-arm-gnueabihf@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz#f23555ee3d8fe941c5c5fd458cd22b65eb1c2232" + integrity sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ== + +"@rollup/rollup-linux-arm-musleabihf@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz#f3bbd1ae2420f5539d40ac1fde2b38da67779baa" + integrity sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg== + +"@rollup/rollup-linux-arm64-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz#7abe900120113e08a1f90afb84c7c28774054d15" + integrity sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw== + +"@rollup/rollup-linux-arm64-musl@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz#9e655285c8175cd44f57d6a1e8e5dedfbba1d820" + integrity sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz#9a79ae6c9e9d8fe83d49e2712ecf4302db5bef5e" + integrity sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg== + +"@rollup/rollup-linux-riscv64-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz#67ac70eca4ace8e2942fabca95164e8874ab8128" + integrity sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA== + +"@rollup/rollup-linux-s390x-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz#9f883a7440f51a22ed7f99e1d070bd84ea5005fc" + integrity sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q== + +"@rollup/rollup-linux-x64-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz#70116ae6c577fe367f58559e2cffb5641a1dd9d0" + integrity sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg== + +"@rollup/rollup-linux-x64-musl@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz#f473f88219feb07b0b98b53a7923be716d1d182f" + integrity sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g== + +"@rollup/rollup-win32-arm64-msvc@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz#4349482d17f5d1c58604d1c8900540d676f420e0" + integrity sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw== + +"@rollup/rollup-win32-ia32-msvc@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz#a6fc39a15db618040ec3c2a24c1e26cb5f4d7422" + integrity sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g== + +"@rollup/rollup-win32-x64-msvc@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz#3dd5d53e900df2a40841882c02e56f866c04d202" + integrity sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q== "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@shikijs/core@1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.18.0.tgz#30dde8e53026dada606c4cf7f32d80a3f33d437c" - integrity sha512-VK4BNVCd2leY62Nm2JjyxtRLkyrZT/tv104O81eyaCjHq4Adceq2uJVFJJAIof6lT1mBwZrEo2qT/T+grv3MQQ== +"@shikijs/core@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.22.0.tgz#74e5d4485e5f7afa85109e322b42e400686f92bb" + integrity sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q== dependencies: - "@shikijs/engine-javascript" "1.18.0" - "@shikijs/engine-oniguruma" "1.18.0" - "@shikijs/types" "1.18.0" - "@shikijs/vscode-textmate" "^9.2.2" + "@shikijs/engine-javascript" "1.22.0" + "@shikijs/engine-oniguruma" "1.22.0" + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" "@types/hast" "^3.0.4" hast-util-to-html "^9.0.3" -"@shikijs/engine-javascript@1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.18.0.tgz#9888011c5d869a687b42e3e56c7243f15a73524b" - integrity sha512-qoP/aO/ATNwYAUw1YMdaip/YVEstMZEgrwhePm83Ll9OeQPuxDZd48szZR8oSQNQBT8m8UlWxZv8EA3lFuyI5A== +"@shikijs/engine-javascript@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz#2e5db29f0421755492f5279f8224ef7a7f907a29" + integrity sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw== dependencies: - "@shikijs/types" "1.18.0" - "@shikijs/vscode-textmate" "^9.2.2" + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" oniguruma-to-js "0.4.3" -"@shikijs/engine-oniguruma@1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.18.0.tgz#7e57fd19b62b18cf2de382da684d042ee934f65d" - integrity sha512-B9u0ZKI/cud+TcmF8Chyh+R4V5qQVvyDOqXC2l2a4x73PBSBc6sZ0JRAX3eqyJswqir6ktwApUUGBYePdKnMJg== +"@shikijs/engine-oniguruma@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz#74c661fac4cd1f08f2c09b5d6e2fd2a6720d0401" + integrity sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw== dependencies: - "@shikijs/types" "1.18.0" - "@shikijs/vscode-textmate" "^9.2.2" + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" -"@shikijs/types@1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.18.0.tgz#4c2d62d17f78cbfc051a15480ab4dfb0f06196c9" - integrity sha512-O9N36UEaGGrxv1yUrN2nye7gDLG5Uq0/c1LyfmxsvzNPqlHzWo9DI0A4+fhW2y3bGKuQu/fwS7EPdKJJCowcVA== +"@shikijs/types@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.22.0.tgz#d2a572381395c9308b472c8199b8e0289753b9ad" + integrity sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww== dependencies: - "@shikijs/vscode-textmate" "^9.2.2" + "@shikijs/vscode-textmate" "^9.3.0" "@types/hast" "^3.0.4" -"@shikijs/vscode-textmate@^9.2.2": - version "9.2.2" - resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz#24571f50625c7cd075f9efe0def8b9d2c0930ada" - integrity sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg== - -"@swc/core-darwin-arm64@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz#2b5cdbd34e4162e50de6147dd1a5cb12d23b08e8" - integrity sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ== - -"@swc/core-darwin-x64@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz#6aa7e3c01ab8e5e41597f8a24ff24c4e50936a46" - integrity sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw== - -"@swc/core-linux-arm-gnueabihf@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz#160108633b9e1d1ad05f815bedc7e9eb5d59fc2a" - integrity sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ== - -"@swc/core-linux-arm64-gnu@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz#cbfa512683c73227ad25552f3b3e722b0e7fbd1d" - integrity sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g== - -"@swc/core-linux-arm64-musl@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz#80239cb58fe57f3c86b44617fe784530ec55ee2b" - integrity sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ== - -"@swc/core-linux-x64-gnu@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz#a699c1632de60b6a63b7fdb7abcb4fef317e57ca" - integrity sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg== - -"@swc/core-linux-x64-musl@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz#8e4c203d6bc41e7f85d7d34d0fdf4ef751fa626c" - integrity sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg== - -"@swc/core-win32-arm64-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz#31e3d42b8c0aa79f0ea1a980c0dd1a999d378ed7" - integrity sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA== - -"@swc/core-win32-ia32-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz#a235285f9f62850aefcf9abb03420f2c54f63638" - integrity sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ== - -"@swc/core-win32-x64-msvc@1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz#f84641393b5223450d00d97bfff877b8b69d7c9b" - integrity sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg== - -"@swc/core@^1.5.7": - version "1.5.7" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.5.7.tgz#e1db7b9887d5f34eb4a3256a738d0c5f1b018c33" - integrity sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ== - dependencies: - "@swc/counter" "^0.1.2" - "@swc/types" "0.1.7" +"@shikijs/vscode-textmate@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz#b2f1776e488c1d6c2b6cd129bab62f71bbc9c7ab" + integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA== + +"@swc/core-darwin-arm64@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz#5f4096c00e71771ca1b18c824f0c92a052c70760" + integrity sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw== + +"@swc/core-darwin-x64@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz#867b7a4f094e6b64201090ca5fcbf3da7d0f3e22" + integrity sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ== + +"@swc/core-linux-arm-gnueabihf@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz#35bb43894def296d92aaa2cc9372d48042f37777" + integrity sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q== + +"@swc/core-linux-arm64-gnu@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz#8e2321cc4ec84cbfed8f8e16ff1ed7b854450443" + integrity sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q== + +"@swc/core-linux-arm64-musl@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz#b1c16e4b23ffa9ff19973eda6ffee35d2a7de7b0" + integrity sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg== + +"@swc/core-linux-x64-gnu@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz#388e2cc13a010cd28787aead2cecf31eb491836d" + integrity sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w== + +"@swc/core-linux-x64-musl@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz#51e0ff30981f26d7a5b97a7a7b5b291bad050d1a" + integrity sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ== + +"@swc/core-win32-arm64-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz#a7fdcc4074c34ee6a026506b594d00323383c11f" + integrity sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA== + +"@swc/core-win32-ia32-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz#ae7be6dde798eebee2000b8fd84e01a439b5bd6a" + integrity sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ== + +"@swc/core-win32-x64-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz#310d607004d7319085a4dec20c0c38c3405cc05b" + integrity sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w== + +"@swc/core@^1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.7.26.tgz#beda9b82063fcec7b56c958804a4d175aecf9a9d" + integrity sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.12" optionalDependencies: - "@swc/core-darwin-arm64" "1.5.7" - "@swc/core-darwin-x64" "1.5.7" - "@swc/core-linux-arm-gnueabihf" "1.5.7" - "@swc/core-linux-arm64-gnu" "1.5.7" - "@swc/core-linux-arm64-musl" "1.5.7" - "@swc/core-linux-x64-gnu" "1.5.7" - "@swc/core-linux-x64-musl" "1.5.7" - "@swc/core-win32-arm64-msvc" "1.5.7" - "@swc/core-win32-ia32-msvc" "1.5.7" - "@swc/core-win32-x64-msvc" "1.5.7" - -"@swc/counter@^0.1.2", "@swc/counter@^0.1.3": + "@swc/core-darwin-arm64" "1.7.26" + "@swc/core-darwin-x64" "1.7.26" + "@swc/core-linux-arm-gnueabihf" "1.7.26" + "@swc/core-linux-arm64-gnu" "1.7.26" + "@swc/core-linux-arm64-musl" "1.7.26" + "@swc/core-linux-x64-gnu" "1.7.26" + "@swc/core-linux-x64-musl" "1.7.26" + "@swc/core-win32-arm64-msvc" "1.7.26" + "@swc/core-win32-ia32-msvc" "1.7.26" + "@swc/core-win32-x64-msvc" "1.7.26" + +"@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/types@0.1.7": - version "0.1.7" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.7.tgz#ea5d658cf460abff51507ca8d26e2d391bafb15e" - integrity sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ== +"@swc/types@^0.1.12": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.12.tgz#7f632c06ab4092ce0ebd046ed77ff7557442282f" + integrity sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA== dependencies: "@swc/counter" "^0.1.3" -"@tabler/icons-react@^3.17.0": - version "3.17.0" - resolved "https://registry.yarnpkg.com/@tabler/icons-react/-/icons-react-3.17.0.tgz#badafce6618f6b8104262538e3f55d34d09c2e29" - integrity sha512-Ndm9Htv7KpIU1PYYrzs5EMhyA3aZGcgaxUp9Q1XOxcRZ+I0X+Ub2WS5f4bkRyDdL1s0++k2T9XRgmg2pG113sw== +"@tabler/icons-react@^3.19.0": + version "3.19.0" + resolved "https://registry.yarnpkg.com/@tabler/icons-react/-/icons-react-3.19.0.tgz#9b5a421bf69875677fde52e7de73b4e6b5571e57" + integrity sha512-AqEWGI0tQWgqo6ZjMO5yJ9sYT8oXLuAM/up0hN9iENS6IdtNZryKrkNSiMgpwweNTpl8wFFG/dAZ959S91A/uQ== dependencies: - "@tabler/icons" "3.17.0" + "@tabler/icons" "3.19.0" -"@tabler/icons@3.17.0": - version "3.17.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.17.0.tgz#329ca3e4cb533c5a6a61467fe5d6de14a0813020" - integrity sha512-sCSfAQ0w93KSnSL7tS08n73CdIKpuHP8foeLMWgDKiZaCs8ZE//N3ytazCk651ZtruTtByI3b+ZDj7nRf+hHvA== +"@tabler/icons@3.19.0": + version "3.19.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.19.0.tgz#5998b0557ef34572e003f2d75ac95e7c04f88c81" + integrity sha512-A4WEWqpdbTfnpFEtwXqwAe9qf9sp1yRPvzppqAuwcoF0q5YInqB+JkJtSFToCyBpPVeLxJUxxkapLvt2qQgnag== -"@tanstack/query-core@5.56.2": - version "5.56.2" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.56.2.tgz#2def2fb0290cd2836bbb08afb0c175595bb8109b" - integrity sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q== +"@tanstack/query-core@5.59.6": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.59.6.tgz#96bf2cc00f2bbd111def52134f10d50e6c1235dd" + integrity sha512-g58YTHe4ClRrjJ50GY9fas/0zARJVozY0Hs+hcSBOmwZaeKY+to0/LX8wKnnH/EJiLYcC1sHmE11CAS3ncfZBg== -"@tanstack/react-query@^5.56.2": - version "5.56.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.56.2.tgz#3a0241b9d010910905382f5e99160997b8795f91" - integrity sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg== +"@tanstack/react-query@^5.59.8": + version "5.59.8" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.59.8.tgz#ae00d67235be9fb6ce79779faa84228e69676041" + integrity sha512-jkvObpbjBL6P/PHFIjvNGG19XyhI8sjP6/Mw7CbmgT6SAps/5fZY5pxDicRwAt1YGCiEQvwrJQ6IdbZ8j5CVfw== dependencies: - "@tanstack/query-core" "5.56.2" + "@tanstack/query-core" "5.59.6" "@testing-library/dom@^10.4.0": version "10.4.0" @@ -1059,10 +1059,10 @@ dependencies: undici-types "~5.26.4" -"@types/node@^22.5.5": - version "22.5.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" - integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA== +"@types/node@^22.7.5": + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== dependencies: undici-types "~6.19.2" @@ -1086,10 +1086,10 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@^18.3.8": - version "18.3.8" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.8.tgz#1672ab19993f8aca7c7dc844c07d5d9e467d5a79" - integrity sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q== +"@types/react@^18.3.11": + version "18.3.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.11.tgz#9d530601ff843ee0d7030d4227ea4360236bd537" + integrity sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -1124,30 +1124,30 @@ resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== -"@typescript-eslint/eslint-plugin@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz#20049754ff9f6d3a09bf240297f029ce04290999" - integrity sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg== +"@typescript-eslint/eslint-plugin@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz#9364b756d4d78bcbdf6fd3e9345e6924c68ad371" + integrity sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.6.0" - "@typescript-eslint/type-utils" "8.6.0" - "@typescript-eslint/utils" "8.6.0" - "@typescript-eslint/visitor-keys" "8.6.0" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/type-utils" "8.8.1" + "@typescript-eslint/utils" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.6.0.tgz#02e092b9dc8b4e319172af620d0d39b337d948f6" - integrity sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow== +"@typescript-eslint/parser@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.8.1.tgz#5952ba2a83bd52024b872f3fdc8ed2d3636073b8" + integrity sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow== dependencies: - "@typescript-eslint/scope-manager" "8.6.0" - "@typescript-eslint/types" "8.6.0" - "@typescript-eslint/typescript-estree" "8.6.0" - "@typescript-eslint/visitor-keys" "8.6.0" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/typescript-estree" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -1166,21 +1166,21 @@ "@typescript-eslint/types" "7.10.0" "@typescript-eslint/visitor-keys" "7.10.0" -"@typescript-eslint/scope-manager@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz#28cc2fc26a84b75addf45091a2c6283e29e2c982" - integrity sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw== +"@typescript-eslint/scope-manager@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz#b4bea1c0785aaebfe3c4ab059edaea1c4977e7ff" + integrity sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA== dependencies: - "@typescript-eslint/types" "8.6.0" - "@typescript-eslint/visitor-keys" "8.6.0" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" -"@typescript-eslint/type-utils@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz#d4347e637478bef88cee1db691fcfa20ade9b8a0" - integrity sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg== +"@typescript-eslint/type-utils@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz#31f59ec46e93a02b409fb4d406a368a59fad306e" + integrity sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA== dependencies: - "@typescript-eslint/typescript-estree" "8.6.0" - "@typescript-eslint/utils" "8.6.0" + "@typescript-eslint/typescript-estree" "8.8.1" + "@typescript-eslint/utils" "8.8.1" debug "^4.3.4" ts-api-utils "^1.3.0" @@ -1194,10 +1194,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.10.0.tgz#da92309c97932a3a033762fd5faa8b067de84e3b" integrity sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg== -"@typescript-eslint/types@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.6.0.tgz#cdc3a16f83f2f0663d6723e9fd032331cdd9f51c" - integrity sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw== +"@typescript-eslint/types@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.8.1.tgz#ebe85e0fa4a8e32a24a56adadf060103bef13bd1" + integrity sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -1226,13 +1226,13 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz#f945506de42871f04868371cb5bf21e8f7266e01" - integrity sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g== +"@typescript-eslint/typescript-estree@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz#34649f4e28d32ee49152193bc7dedc0e78e5d1ec" + integrity sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg== dependencies: - "@typescript-eslint/types" "8.6.0" - "@typescript-eslint/visitor-keys" "8.6.0" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -1240,15 +1240,15 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.6.0.tgz#175fe893f32804bed1e72b3364ea6bbe1044181c" - integrity sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A== +"@typescript-eslint/utils@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.8.1.tgz#9e29480fbfa264c26946253daa72181f9f053c9d" + integrity sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.6.0" - "@typescript-eslint/types" "8.6.0" - "@typescript-eslint/typescript-estree" "8.6.0" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/typescript-estree" "8.8.1" "@typescript-eslint/utils@^5.58.0": version "5.62.0" @@ -1290,12 +1290,12 @@ "@typescript-eslint/types" "7.10.0" eslint-visitor-keys "^3.4.3" -"@typescript-eslint/visitor-keys@8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz#5432af4a1753f376f35ab5b891fc9db237aaf76f" - integrity sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg== +"@typescript-eslint/visitor-keys@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz#0fb1280f381149fc345dfde29f7542ff4e587fc5" + integrity sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag== dependencies: - "@typescript-eslint/types" "8.6.0" + "@typescript-eslint/types" "8.8.1" eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": @@ -1303,17 +1303,17 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitejs/plugin-react-swc@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz#e456c0a6d7f562268e1d231af9ac46b86ef47d88" - integrity sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA== +"@vitejs/plugin-react-swc@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.1.tgz#bc5af48ef35c525d623fa14177c912cbaa86a846" + integrity sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg== dependencies: - "@swc/core" "^1.5.7" + "@swc/core" "^1.7.26" -"@vitest/coverage-istanbul@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.1.tgz#11950fea0bff2628f3c278e2dac338e4af69c521" - integrity sha512-ZQM8uLinwmhmLp49fxLxIM46nC7NisCbaiydcQoV1hLvQfFL92Gg3tInRvowZyV78G0IknjN10JzH7oqPlPjZw== +"@vitest/coverage-istanbul@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.2.tgz#9c0beddef5a146b7cbf39781cfd6f6dde6e3b3c8" + integrity sha512-dg7ex3GKrTIenAV0oEp78JucTVFsPMzjl1gYWun22O7SBDxcHFC/REZjWLhMTHRHY8ihm4uBCvmu+CvEu5/Adg== dependencies: "@istanbuljs/schema" "^0.1.3" debug "^4.3.6" @@ -1326,53 +1326,53 @@ test-exclude "^7.0.1" tinyrainbow "^1.2.0" -"@vitest/expect@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.1.tgz#907137a86246c5328929d796d741c4e95d1ee19d" - integrity sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w== +"@vitest/expect@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.2.tgz#e92fa284b8472548f72cacfe896020c64af6bf78" + integrity sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg== dependencies: - "@vitest/spy" "2.1.1" - "@vitest/utils" "2.1.1" + "@vitest/spy" "2.1.2" + "@vitest/utils" "2.1.2" chai "^5.1.1" tinyrainbow "^1.2.0" -"@vitest/mocker@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.1.tgz#3e37c80ac267318d4aa03c5073a017d148dc8e67" - integrity sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA== +"@vitest/mocker@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.2.tgz#08853a9d8d12afba284aebdf9b5ea26ddae5f20a" + integrity sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA== dependencies: "@vitest/spy" "^2.1.0-beta.1" estree-walker "^3.0.3" magic-string "^0.30.11" -"@vitest/pretty-format@2.1.1", "@vitest/pretty-format@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.1.tgz#fea25dd4e88c3c1329fbccd1d16b1d607eb40067" - integrity sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ== +"@vitest/pretty-format@2.1.2", "@vitest/pretty-format@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.2.tgz#42882ea18c4cd40428e34f74bbac706a82465193" + integrity sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA== dependencies: tinyrainbow "^1.2.0" -"@vitest/runner@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.1.tgz#f3b1fbc3c109fc44e2cceecc881344453f275559" - integrity sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA== +"@vitest/runner@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.2.tgz#14da1f5eac43fbd9a37d7cd72de102e8f785d727" + integrity sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw== dependencies: - "@vitest/utils" "2.1.1" + "@vitest/utils" "2.1.2" pathe "^1.1.2" -"@vitest/snapshot@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.1.tgz#38ef23104e90231fea5540754a19d8468afbba66" - integrity sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw== +"@vitest/snapshot@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.2.tgz#e20bd794b33fdcd4bfe69138baac7bb890c4d51f" + integrity sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA== dependencies: - "@vitest/pretty-format" "2.1.1" + "@vitest/pretty-format" "2.1.2" magic-string "^0.30.11" pathe "^1.1.2" -"@vitest/spy@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.1.tgz#20891f7421a994256ea0d739ed72f05532c78488" - integrity sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g== +"@vitest/spy@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.2.tgz#bccdeca597c8fc3777302889e8c98cec9264df44" + integrity sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A== dependencies: tinyspy "^3.0.0" @@ -1383,12 +1383,12 @@ dependencies: tinyspy "^3.0.0" -"@vitest/ui@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-2.1.1.tgz#3d2b3c4e2f8f30c3615e731e0c63510799546b94" - integrity sha512-IIxo2LkQDA+1TZdPLYPclzsXukBWd5dX2CKpGqH8CCt8Wh0ZuDn4+vuQ9qlppEju6/igDGzjWF/zyorfsf+nHg== +"@vitest/ui@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-2.1.2.tgz#039d2a264ea517cb2f5b8a6a5d806c6a7c73f9a2" + integrity sha512-92gcNzkDnmxOxyHzQrQYRsoV9Q0Aay0r4QMLnV+B+lbqlUWa8nDg9ivyLV5mMVTtGirHsYUGGh/zbIA55gBZqA== dependencies: - "@vitest/utils" "2.1.1" + "@vitest/utils" "2.1.2" fflate "^0.8.2" flatted "^3.3.1" pathe "^1.1.2" @@ -1396,12 +1396,12 @@ tinyglobby "^0.2.6" tinyrainbow "^1.2.0" -"@vitest/utils@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.1.tgz#284d016449ecb4f8704d198d049fde8360cc136e" - integrity sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ== +"@vitest/utils@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.2.tgz#222ac35ba02493173e40581256eb7a62520fcdba" + integrity sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ== dependencies: - "@vitest/pretty-format" "2.1.1" + "@vitest/pretty-format" "2.1.2" loupe "^3.1.1" tinyrainbow "^1.2.0" @@ -2308,24 +2308,24 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-module-utils@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz#b99b211ca4318243f09661fae088f373ad5243c4" - integrity sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ== +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== dependencies: debug "^3.2.7" -eslint-plugin-html@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-8.1.1.tgz#504444f39e3ccb64502ac68dc6c534567aa56492" - integrity sha512-6qmlJsc40D2m3Dn9oEH+0PAOkJhxVu0f5sVItqpCE0YWgYnyP4xCjBc3UWTHaJcY9ARkWOLIIuXLq0ndRnQOHw== +eslint-plugin-html@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-8.1.2.tgz#3a7a092d1e19e7e494014ed0747c5873856d7e1c" + integrity sha512-pbRchDV2SmqbCi/Ev/q3aAikzG9BcFe0IjjqjtMn8eTLq71ZUggyJB6CDmuwGAXmYZHrXI12XTfCqvgcnPRqGw== dependencies: htmlparser2 "^9.1.0" -eslint-plugin-import@^2.30.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449" - integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== +eslint-plugin-import@^2.31.0: + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== dependencies: "@rtsao/scc" "^1.1.0" array-includes "^3.1.8" @@ -2335,7 +2335,7 @@ eslint-plugin-import@^2.30.0: debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.9.0" + eslint-module-utils "^2.12.0" hasown "^2.0.2" is-core-module "^2.15.1" is-glob "^4.0.3" @@ -2344,6 +2344,7 @@ eslint-plugin-import@^2.30.0: object.groupby "^1.0.3" object.values "^1.2.0" semver "^6.3.1" + string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" eslint-plugin-jsx-a11y@^6.10.0: @@ -2386,10 +2387,10 @@ eslint-plugin-react-hooks@^4.6.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -eslint-plugin-react@^7.36.1: - version "7.36.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz#f1dabbb11f3d4ebe8b0cf4e54aff4aee81144ee5" - integrity sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA== +eslint-plugin-react@^7.37.1: + version "7.37.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" + integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" @@ -3258,10 +3259,10 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jose@^5.9.2: - version "5.9.2" - resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.2.tgz#22a22da06edb8fb9e583aa24bafc1e8457b4db92" - integrity sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ== +jose@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.3.tgz#6eba1ee3f70b42891f0e1883fe0084a46dbbe02c" + integrity sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -4126,28 +4127,28 @@ rimraf@^3.0.2: glob "^7.1.3" rollup@^4.20.0: - version "4.21.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.3.tgz#c64ba119e6aeb913798a6f7eef2780a0df5a0821" - integrity sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA== + version "4.22.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.22.4.tgz#4135a6446671cd2a2453e1ad42a45d5973ec3a0f" + integrity sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A== dependencies: "@types/estree" "1.0.5" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.21.3" - "@rollup/rollup-android-arm64" "4.21.3" - "@rollup/rollup-darwin-arm64" "4.21.3" - "@rollup/rollup-darwin-x64" "4.21.3" - "@rollup/rollup-linux-arm-gnueabihf" "4.21.3" - "@rollup/rollup-linux-arm-musleabihf" "4.21.3" - "@rollup/rollup-linux-arm64-gnu" "4.21.3" - "@rollup/rollup-linux-arm64-musl" "4.21.3" - "@rollup/rollup-linux-powerpc64le-gnu" "4.21.3" - "@rollup/rollup-linux-riscv64-gnu" "4.21.3" - "@rollup/rollup-linux-s390x-gnu" "4.21.3" - "@rollup/rollup-linux-x64-gnu" "4.21.3" - "@rollup/rollup-linux-x64-musl" "4.21.3" - "@rollup/rollup-win32-arm64-msvc" "4.21.3" - "@rollup/rollup-win32-ia32-msvc" "4.21.3" - "@rollup/rollup-win32-x64-msvc" "4.21.3" + "@rollup/rollup-android-arm-eabi" "4.22.4" + "@rollup/rollup-android-arm64" "4.22.4" + "@rollup/rollup-darwin-arm64" "4.22.4" + "@rollup/rollup-darwin-x64" "4.22.4" + "@rollup/rollup-linux-arm-gnueabihf" "4.22.4" + "@rollup/rollup-linux-arm-musleabihf" "4.22.4" + "@rollup/rollup-linux-arm64-gnu" "4.22.4" + "@rollup/rollup-linux-arm64-musl" "4.22.4" + "@rollup/rollup-linux-powerpc64le-gnu" "4.22.4" + "@rollup/rollup-linux-riscv64-gnu" "4.22.4" + "@rollup/rollup-linux-s390x-gnu" "4.22.4" + "@rollup/rollup-linux-x64-gnu" "4.22.4" + "@rollup/rollup-linux-x64-musl" "4.22.4" + "@rollup/rollup-win32-arm64-msvc" "4.22.4" + "@rollup/rollup-win32-ia32-msvc" "4.22.4" + "@rollup/rollup-win32-x64-msvc" "4.22.4" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -4227,16 +4228,16 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.18.0.tgz#4f9ca2f442b3612849017ab1dcac47c35ee52276" - integrity sha512-8jo7tOXr96h9PBQmOHVrltnETn1honZZY76YA79MHheGQg55jBvbm9dtU+MI5pjC5NJCFuA6rvVTLVeSW5cE4A== +shiki@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.22.0.tgz#45d1dfff0e03a598af70e2ec8592f14ef07827b4" + integrity sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw== dependencies: - "@shikijs/core" "1.18.0" - "@shikijs/engine-javascript" "1.18.0" - "@shikijs/engine-oniguruma" "1.18.0" - "@shikijs/types" "1.18.0" - "@shikijs/vscode-textmate" "^9.2.2" + "@shikijs/core" "1.22.0" + "@shikijs/engine-javascript" "1.22.0" + "@shikijs/engine-oniguruma" "1.22.0" + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" "@types/hast" "^3.0.4" side-channel@^1.0.4, side-channel@^1.0.6: @@ -4679,19 +4680,19 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript-eslint@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.6.0.tgz#5f0b5e23b34385ef146e447616c1a0d6bd14bedb" - integrity sha512-eEhhlxCEpCd4helh3AO1hk0UP2MvbRi9CtIAJTVPQjuSXOOO2jsEacNi4UdcJzZJbeuVg1gMhtZ8UYb+NFYPrA== +typescript-eslint@^8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.8.1.tgz#b375c877b2184d883b6228170bc66f0fca847c9a" + integrity sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ== dependencies: - "@typescript-eslint/eslint-plugin" "8.6.0" - "@typescript-eslint/parser" "8.6.0" - "@typescript-eslint/utils" "8.6.0" + "@typescript-eslint/eslint-plugin" "8.8.1" + "@typescript-eslint/parser" "8.8.1" + "@typescript-eslint/utils" "8.8.1" -typescript@^5.6.2: - version "5.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" - integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +typescript@^5.6.3: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== unbox-primitive@^1.0.2: version "1.0.2" @@ -4842,10 +4843,10 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -vite-node@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.1.tgz#7d46f623c04dfed6df34e7127711508a3386fa1c" - integrity sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA== +vite-node@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.2.tgz#f5491a2b399959c9e2f3c4b70cb0cbaecf9be6d2" + integrity sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ== dependencies: cac "^6.7.14" debug "^4.3.6" @@ -4902,10 +4903,10 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" -vite@^5.4.7: - version "5.4.7" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.7.tgz#d226f57c08b61379e955f3836253ed3efb2dcf00" - integrity sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ== +vite@^5.4.8: + version "5.4.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8" + integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ== dependencies: esbuild "^0.21.3" postcss "^8.4.43" @@ -4918,18 +4919,18 @@ vitest-sonar-reporter@^2.0.0: resolved "https://registry.yarnpkg.com/vitest-sonar-reporter/-/vitest-sonar-reporter-2.0.0.tgz#6372e5faba2b2834eac0b2cd1283263d38d0e718" integrity sha512-LorC3NnmrBrryx4+l3BEsNQjD0Y7wfmrD1y/+tHDuZUuVj7w8nOxRXCBSppDfmgfpToOhwchh0JcL4IGMKUKDA== -vitest@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.1.tgz#24a6f6f5d894509f10685b82de008c507faacbb1" - integrity sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA== - dependencies: - "@vitest/expect" "2.1.1" - "@vitest/mocker" "2.1.1" - "@vitest/pretty-format" "^2.1.1" - "@vitest/runner" "2.1.1" - "@vitest/snapshot" "2.1.1" - "@vitest/spy" "2.1.1" - "@vitest/utils" "2.1.1" +vitest@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.2.tgz#f285fdde876749fddc0cb4d9748ae224443c1694" + integrity sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A== + dependencies: + "@vitest/expect" "2.1.2" + "@vitest/mocker" "2.1.2" + "@vitest/pretty-format" "^2.1.2" + "@vitest/runner" "2.1.2" + "@vitest/snapshot" "2.1.2" + "@vitest/spy" "2.1.2" + "@vitest/utils" "2.1.2" chai "^5.1.1" debug "^4.3.6" magic-string "^0.30.11" @@ -4940,7 +4941,7 @@ vitest@^2.1.1: tinypool "^1.0.0" tinyrainbow "^1.2.0" vite "^5.0.0" - vite-node "2.1.1" + vite-node "2.1.2" why-is-node-running "^2.3.0" vscode-jsonrpc@6.0.0: diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index fd31157a..555a684f 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -18,8 +18,8 @@ runtime; build; native; contentfiles; analyzers - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Serilog.Ui.Common.Tests/Serilog.Ui.Common.Tests.csproj b/tests/Serilog.Ui.Common.Tests/Serilog.Ui.Common.Tests.csproj index 41d9b3f3..625cd480 100644 --- a/tests/Serilog.Ui.Common.Tests/Serilog.Ui.Common.Tests.csproj +++ b/tests/Serilog.Ui.Common.Tests/Serilog.Ui.Common.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/Serilog.Ui.ElasticSearchProvider.Tests/Serilog.Ui.ElasticSearchProvider.Tests.csproj b/tests/Serilog.Ui.ElasticSearchProvider.Tests/Serilog.Ui.ElasticSearchProvider.Tests.csproj index 9bd4ca9b..f773c9e1 100644 --- a/tests/Serilog.Ui.ElasticSearchProvider.Tests/Serilog.Ui.ElasticSearchProvider.Tests.csproj +++ b/tests/Serilog.Ui.ElasticSearchProvider.Tests/Serilog.Ui.ElasticSearchProvider.Tests.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj b/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj index 6b181e90..0564f22b 100644 --- a/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj +++ b/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs index 34ab68d9..6278d88b 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -7,42 +7,39 @@ using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MsSqlServerProvider; +using Serilog.Ui.MsSqlServerProvider.Extensions; using Xunit; -namespace MsSql.Tests.DataProvider +namespace MsSql.Tests.DataProvider; + +[Trait("Unit-Base", "MsSql")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "MsSql")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact(Skip = "Not required")] + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public async Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var suts = new List - { - () => { _ = new SqlServerDataProvider(null!); }, - () => { _ = new SqlServerDataProvider(null!); }, - }; + // Arrange + SqlServerDataProvider sut = new( + new SqlServerDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new SqlServerQueryBuilder()); - suts.ForEach(sut => sut.Should().ThrowExactly()); - } + SqlServerDataProvider sutWithCols = new( + new SqlServerDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new SqlServerQueryBuilder()); - [Fact] - public async Task It_logs_and_throws_when_db_read_breaks_down() - { - // Arrange - var sut = new SqlServerDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var sutWithAdditionalCols = - new SqlServerDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var query = new Dictionary { ["page"] = "1", ["count"] = "10", }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - // Act - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithAdditionalCols = () => sutWithAdditionalCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - await assert.Should().ThrowExactlyAsync(); - await assertWithAdditionalCols.Should().ThrowExactlyAsync(); - } + // Assert + await assert.Should().ThrowExactlyAsync(); + await assertWithCols.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs new file mode 100644 index 00000000..1c3ce788 --- /dev/null +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using MsSql.Tests.Util; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MsSqlServerProvider; +using Serilog.Ui.MsSqlServerProvider.Models; +using Xunit; + +namespace MsSql.Tests.DataProvider; + +[Trait("Unit-QueryBuilder", "MsSql")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForSink_ReturnsCorrectQuery( + string schema, + string tableName, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate, + string expectedQuery) + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = level, + ["search"] = searchCriteria, + ["startDate"] = startDate?.ToString("O"), + ["endDate"] = endDate?.ToString("O") + }; + + SqlServerSinkColumnNames sinkColumns = new(); + SqlServerQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + [Fact] + public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = "level", + ["search"] = "criteria" + }; + + SqlServerSinkColumnNames sinkColumns = new(); + SqlServerQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.ToLowerInvariant().Should().StartWith("select"); + query.ToLowerInvariant().Should().NotContain("exception"); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + "dbo", "logs", null!, null!, null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ], + [ + "dbo", "logs", null!, null!, null!, DateTime.Now, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ], + [ + "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ], + [ + "dbo", "logs", "Information", null!, null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [Level] = @Level ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ], + [ + "dbo", "logs", null!, "Test", null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ], + [ + "dbo", "logs", "Information", "Test", null!, null!, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ], + [ + "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, + "SELECT [Id], [Message], [Level], [TimeStamp], [Exception], [Properties] FROM [dbo].[logs] WHERE 1 = 1 AND [Level] = @Level AND ([Message] LIKE @Search OR [Exception] LIKE @Search) AND [TimeStamp] >= @StartDate AND [TimeStamp] <= @EndDate ORDER BY [TimeStamp] DESC OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj index 18a51528..ac334f2b 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Serilog.Ui.MsSqlServerProvider.Tests.csproj @@ -1,5 +1,4 @@  - MsSql.Tests MsSql.Tests diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs index 80c1c6d1..9ba7ec99 100644 --- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs +++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/Util/MsSqlServerTestProvider.cs @@ -8,8 +8,8 @@ using Serilog.Ui.Common.Tests.DataSamples; using Serilog.Ui.Common.Tests.SqlUtil; using Serilog.Ui.Core.Extensions; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MsSqlServerProvider; +using Serilog.Ui.MsSqlServerProvider.Extensions; using Testcontainers.MsSql; using Xunit; @@ -32,11 +32,12 @@ protected MsSqlServerTestProvider() .ForUnixContainer() .UntilCommandIsCompleted("/opt/mssql-tools18/bin/sqlcmd", "-C", "-Q", "SELECT 1;"); Container = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") .WithWaitStrategy(waitStrategy) .Build(); } - private RelationalDbOptions DbOptions { get; } = new RelationalDbOptions("dbo").WithTable("Logs"); + private SqlServerDbOptions DbOptions { get; } = new SqlServerDbOptions("dbo").WithTable("Logs"); protected override sealed IContainer Container { get; set; } @@ -69,8 +70,11 @@ protected override Task InitializeAdditionalAsync() Collector = serilog.InitializeLogs(); SqlMapper.AddTypeHandler(new DapperDateTimeHandler()); + var custom = typeof(T) != typeof(SqlServerLogModel); - Provider = custom ? new SqlServerDataProvider(DbOptions) : new SqlServerDataProvider(DbOptions); + Provider = custom + ? new SqlServerDataProvider(DbOptions, new SqlServerQueryBuilder()) + : new SqlServerDataProvider(DbOptions, new SqlServerQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs index 699340dd..e7de7a5a 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/DataProviderBaseTest.cs @@ -7,40 +7,39 @@ using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Xunit; -namespace MySql.Tests.DataProvider.MariaDb +namespace MySql.Tests.DataProvider.MariaDb; + +[Trait("Unit-Base", "MariaDb")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "MariaDb")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact(Skip = "Not required")] + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public async Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var suts = new List - { - () => { _ = new MariaDbDataProvider(null!); }, - () => { _ = new MariaDbDataProvider(null!); }, - }; + // Arrange + MariaDbDataProvider sut = new( + new MariaDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new MySqlQueryBuilder()); - suts.ForEach(sut => sut.Should().ThrowExactly()); - } + MariaDbDataProvider sutWithCols = new( + new MariaDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new MySqlQueryBuilder()); - [Fact] - public async Task It_logs_and_throws_when_db_read_breaks_down() - { - var sut = new MariaDbDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var sutWithCols = - new MariaDbDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); - var query = new Dictionary { ["page"] = "1", ["count"] = "10", }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithAdditionalCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - await assert.Should().ThrowExactlyAsync(); - await assertWithAdditionalCols.Should().ThrowExactlyAsync(); - } + // Assert + await assert.Should().ThrowExactlyAsync(); + await assertWithCols.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs new file mode 100644 index 00000000..f780be59 --- /dev/null +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MariaDb/QueryBuilderTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using MySql.Tests.Util; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Models; +using Xunit; + +namespace MySql.Tests.DataProvider.MariaDb; + +[Trait("Unit-QueryBuilder", "MariaDb")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForMariaDblSink_ReturnsCorrectQuery( + string schema, + string tableName, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate, + string expectedQuery) + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = level, + ["search"] = searchCriteria, + ["startDate"] = startDate?.ToString("O"), + ["endDate"] = endDate?.ToString("O") + }; + + MariaDbSinkColumnNames sinkColumns = new(); + MySqlQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + [Fact] + public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = "level", + ["search"] = "criteria" + }; + + MySqlSinkColumnNames sinkColumns = new(); + MySqlQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.ToLowerInvariant().Should().StartWith("select"); + query.ToLowerInvariant().Should().NotContain("exception"); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + "dbo", "logs", null!, null!, null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, null!, DateTime.Now, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp >= @StartDate AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", null!, null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND LogLevel = @Level ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, "Test", null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", null!, null!, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND LogLevel = @Level AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, + "SELECT Id, Message, LogLevel, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND LogLevel = @Level AND (Message LIKE @Search OR Exception LIKE @Search) AND TimeStamp >= @StartDate AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs index 595bffb9..7f4fb759 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/DataProviderBaseTest.cs @@ -6,34 +6,33 @@ using Serilog.Ui.Common.Tests.TestSuites; using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Xunit; -namespace MySql.Tests.DataProvider.MySql +namespace MySql.Tests.DataProvider.MySql; + +[Trait("Unit-Base", "MySql")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "MySql")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact(Skip = "Not required")] + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var suts = new List> - { - () => new MySqlDataProvider(null!), - }; + // Arrange + MySqlDataProvider sut = new( + new MySqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new MySqlQueryBuilder()); - suts.ForEach(sut => sut.Should().ThrowExactly()); - } + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - [Fact] - public Task It_logs_and_throws_when_db_read_breaks_down() - { - var sut = new MySqlDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs")); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var query = new Dictionary { ["page"] = "1", ["count"] = "10", }; - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - return assert.Should().ThrowExactlyAsync(); - } + // Assert + return assert.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/QueryBuilderTests.cs b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/QueryBuilderTests.cs new file mode 100644 index 00000000..46a8d3d3 --- /dev/null +++ b/tests/Serilog.Ui.MySqlProvider.Tests/DataProvider/MySql/QueryBuilderTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Serilog.Ui.Core.Models; +using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Models; +using Xunit; + +namespace MySql.Tests.DataProvider.MySql; + +[Trait("Unit-QueryBuilder", "MySql")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForMySqlSink_ReturnsCorrectQuery( + string schema, + string tableName, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate, + string expectedQuery) + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = level, + ["search"] = searchCriteria, + ["startDate"] = startDate?.ToString("O"), + ["endDate"] = endDate?.ToString("O") + }; + + MySqlSinkColumnNames sinkColumns = new(); + MySqlQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + "dbo", "logs", null!, null!, null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, null!, DateTime.Now, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND TimeStamp >= @StartDate AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", null!, null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND Level = @Level ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", null!, "Test", null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", null!, null!, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND Level = @Level AND (Message LIKE @Search OR Exception LIKE @Search) ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ], + [ + "dbo", "logs", "Information", "Test", DateTime.Now, DateTime.Now, + "SELECT Id, Message, Level, TimeStamp, Exception, Properties FROM logs WHERE TRUE AND Level = @Level AND (Message LIKE @Search OR Exception LIKE @Search) AND TimeStamp >= @StartDate AND TimeStamp <= @EndDate ORDER BY TimeStamp DESC LIMIT @Offset, @Count" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj index 8f0197aa..ee69e498 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Serilog.Ui.MySqlProvider.Tests.csproj @@ -1,5 +1,4 @@  - MySql.Tests MySql.Tests diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs index da3f95e6..7a66fff0 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MariaDbTestProvider.cs @@ -9,8 +9,8 @@ using Serilog.Ui.Common.Tests.DataSamples; using Serilog.Ui.Common.Tests.SqlUtil; using Serilog.Ui.Core.Extensions; -using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Testcontainers.MariaDb; using Xunit; @@ -29,9 +29,10 @@ public class MariaDbTestProvider : DatabaseInstance protected MariaDbTestProvider() { Container = new MariaDbBuilder().Build(); + DbOptions = new MariaDbOptions("dbo").WithTable("Logs"); } - private RelationalDbOptions DbOptions { get; set; } = new RelationalDbOptions("dbo").WithTable("Logs"); + private MariaDbOptions DbOptions { get; } protected override sealed IContainer Container { get; set; } @@ -50,7 +51,7 @@ protected override async Task CheckDbReadinessAsync() protected override Task InitializeAdditionalAsync() { - var serilog = new SerilogSinkSetup(logger => + SerilogSinkSetup serilog = new(logger => { logger .WriteTo.MariaDB( @@ -62,10 +63,13 @@ protected override Task InitializeAdditionalAsync() PropertiesToColumnsMapping = PropertiesToColumnsMapping }); }); + Collector = serilog.InitializeLogs(); var custom = typeof(T) != typeof(MySqlLogModel); - Provider = custom ? new MariaDbDataProvider(DbOptions) : new MariaDbDataProvider(DbOptions); + Provider = custom + ? new MariaDbDataProvider(DbOptions, new MySqlQueryBuilder()) + : new MariaDbDataProvider(DbOptions, new MySqlQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs index 647dfad1..2cb9bfdb 100644 --- a/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs +++ b/tests/Serilog.Ui.MySqlProvider.Tests/Util/MySqlTestProvider.cs @@ -8,6 +8,7 @@ using Serilog.Ui.Core.Extensions; using Serilog.Ui.Core.Models.Options; using Serilog.Ui.MySqlProvider; +using Serilog.Ui.MySqlProvider.Extensions; using Testcontainers.MySql; using Xunit; @@ -25,9 +26,10 @@ public sealed class MySqlTestProvider : DatabaseInstance public MySqlTestProvider() { Container = new MySqlBuilder().Build(); + DbOptions = new MySqlDbOptions("dbo").WithTable("Logs"); } - public RelationalDbOptions DbOptions { get; set; } = new RelationalDbOptions("dbo").WithTable("Logs"); + public MySqlDbOptions DbOptions { get; } protected override async Task CheckDbReadinessAsync() { @@ -42,13 +44,14 @@ protected override async Task CheckDbReadinessAsync() protected override Task InitializeAdditionalAsync() { - var serilog = new SerilogSinkSetup(logger => + SerilogSinkSetup serilog = new(logger => { logger.WriteTo.MySQL(DbOptions.ConnectionString, batchSize: 1, storeTimestampInUtc: true); }); + Collector = serilog.InitializeLogs(); - Provider = new MySqlDataProvider(DbOptions); + Provider = new MySqlDataProvider(DbOptions, new MySqlQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs index cd2b0bce..aa679ead 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -9,41 +9,38 @@ using Serilog.Ui.Core.Models; using Serilog.Ui.PostgreSqlProvider; using Serilog.Ui.PostgreSqlProvider.Extensions; +using Serilog.Ui.PostgreSqlProvider.Models; using Xunit; -namespace Postgres.Tests.DataProvider +namespace Postgres.Tests.DataProvider; + +[Trait("Unit-Base", "Postgres")] +public class DataProviderBaseTest : IUnitBaseTests { - [Trait("Unit-Base", "Postgres")] - public class DataProviderBaseTest : IUnitBaseTests + [Fact(Skip = "Not required")] + public void It_throws_when_any_dependency_is_null() + => throw new NotImplementedException(); + + [Fact] + public async Task It_logs_and_throws_when_db_read_breaks_down() { - [Fact] - public void It_throws_when_any_dependency_is_null() - { - var sut = new List - { - () => { _ = new PostgresDataProvider(null!); }, - () => { _ = new PostgresDataProvider(null!); }, - }; + // Arrange + PostgresDataProvider sut = new( + new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new PostgresQueryBuilder()); - sut.ForEach(s => s.Should().ThrowExactly()); - } + PostgresDataProvider sutWithCols = new( + new PostgreSqlDbOptions("dbo").WithConnectionString("connString").WithTable("logs"), + new PostgresQueryBuilder()); - [Fact] - public async Task It_logs_and_throws_when_db_read_breaks_down() - { - var sut = new PostgresDataProvider(new PostgreSqlDbOptions("dbo") - .WithConnectionString("connString") - .WithTable("logs")); - var sutWithCols = new PostgresDataProvider(new PostgreSqlDbOptions("dbo") - .WithConnectionString("connString") - .WithTable("logs")); - var query = new Dictionary { ["page"] = "1", ["count"] = "10" }; + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; - var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + // Act + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); - await assert.Should().ThrowExactlyAsync(); - await assertWithCols.Should().ThrowExactlyAsync(); - } + // Assert + await assert.Should().ThrowExactlyAsync(); + await assertWithCols.Should().ThrowExactlyAsync(); } } \ No newline at end of file diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs index 52d954f7..7e7725e6 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -14,8 +14,6 @@ namespace Postgres.Tests.DataProvider; [Trait("Unit-QueryBuilder", "Postgres")] public class QueryBuilderTests { - private readonly PostgreSqlAlternativeSinkColumnNames _sut = new(); - [Theory] [ClassData(typeof(QueryBuilderTestData))] public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( @@ -28,7 +26,7 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( string expectedQuery) { // Arrange - var queryLogs = new Dictionary + Dictionary queryLogs = new() { ["level"] = level, ["search"] = searchCriteria, @@ -36,8 +34,11 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( ["endDate"] = endDate?.ToString("O") }; + PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + PostgresQueryBuilder sut = new(); + // Act - var query = _sut.BuildFetchLogsQuery(schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); // Assert query.Should().Be(expectedQuery); @@ -47,17 +48,20 @@ public void BuildFetchLogsQuery_ForAlternativeSink_ReturnsCorrectQuery( public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model() { // Arrange - var queryLogs = new Dictionary + Dictionary queryLogs = new() { ["level"] = "level", ["search"] = "criteria" }; + PostgreSqlAlternativeSinkColumnNames sinkColumns = new(); + PostgresQueryBuilder sut = new(); + // Act - var query = _sut.BuildFetchLogsQuery("test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); + string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs)); // Assert - query.ToLowerInvariant().Should().StartWith("select *"); + query.ToLowerInvariant().Should().StartWith("select"); query.ToLowerInvariant().Should().NotContain("exception"); } diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj index 9f1db41b..c5e708c9 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Serilog.Ui.PostgreSqlProvider.Tests.csproj @@ -1,8 +1,7 @@  - - Postgres.Tests - Postgres.Tests + Postgres.Tests + Postgres.Tests @@ -22,5 +21,4 @@ - diff --git a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs index 6bd8c3da..87325b8c 100644 --- a/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs +++ b/tests/Serilog.Ui.PostgreSqlProvider.Tests/Util/PostgresTestProvider.cs @@ -31,7 +31,7 @@ protected PostgresTestProvider() Container = new PostgreSqlBuilder().Build(); } - private PostgreSqlDbOptions DbOptions { get; set; } = new PostgreSqlDbOptions("public") + private PostgreSqlDbOptions DbOptions { get; } = new PostgreSqlDbOptions("public") .WithTable("logs") .WithSinkType(PostgreSqlSinkType.SerilogSinksPostgreSQLAlternative); @@ -50,7 +50,7 @@ protected override async Task CheckDbReadinessAsync() protected override Task InitializeAdditionalAsync() { - var serilog = new SerilogSinkSetup(logger => + SerilogSinkSetup serilog = new(logger => { logger .WriteTo.PostgreSQL( @@ -66,7 +66,9 @@ protected override Task InitializeAdditionalAsync() Collector = serilog.InitializeLogs(); var custom = typeof(T) != typeof(PostgresLogModel); - Provider = custom ? new PostgresDataProvider(DbOptions) : new PostgresDataProvider(DbOptions); + Provider = custom + ? new PostgresDataProvider(DbOptions, new PostgresQueryBuilder()) + : new PostgresDataProvider(DbOptions, new PostgresQueryBuilder()); return Task.CompletedTask; } diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs new file mode 100644 index 00000000..a5239030 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderBaseTest.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Serilog.Ui.Common.Tests.TestSuites; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.Core.Models; +using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Extensions; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Sqlite.Tests.DataProvider +{ + [Trait("Unit-Base", "Sqlite")] + public class DataProviderBaseTest : IUnitBaseTests + { + [Fact] + public void It_throws_when_any_dependency_is_null() + { + var suts = new List> + { + () => new SqliteDataProvider(null!, new SqliteQueryBuilder()), + }; + + suts.ForEach(sut => sut.Should().ThrowExactly()); + } + + [Fact] + public Task It_logs_and_throws_when_db_read_breaks_down() + { + var sut = new SqliteDataProvider( + new SqliteDbOptions().WithConnectionString("connString").WithTable("Logs"), + new SqliteQueryBuilder() + ); + + Dictionary query = new() { ["page"] = "1", ["count"] = "10" }; + + var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + return assert.Should().ThrowExactlyAsync(); + } + } +} diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs new file mode 100644 index 00000000..ecfba61d --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderPaginationTest.cs @@ -0,0 +1,24 @@ +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Primitives; +using Serilog.Ui.Common.Tests.TestSuites.Impl; +using Serilog.Ui.Core.Models; +using Sqlite.Tests.Util; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Sqlite.Tests.DataProvider; + +[Collection(nameof(SqliteTestProvider))] +[Trait("Integration-Pagination", "Sqlite")] +public class DataProviderPaginationTest(SqliteTestProvider instance) : IntegrationPaginationTests(instance) +{ + [Fact] + public override Task It_throws_when_skip_is_zero() + { + var query = new Dictionary { ["page"] = "0", ["count"] = "1" }; + var test = () => Provider.FetchDataAsync(FetchLogsQuery.ParseQuery(query)); + return test.Should().NotThrowAsync("because Sqlite catches the error"); + } +} diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs new file mode 100644 index 00000000..f0a709c8 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/DataProviderSearchTest.cs @@ -0,0 +1,10 @@ +using Serilog.Ui.Common.Tests.TestSuites.Impl; +using Serilog.Ui.SqliteDataProvider; +using Sqlite.Tests.Util; +using Xunit; + +namespace Sqlite.Tests.DataProvider; + +[Collection(nameof(SqliteTestProvider))] +[Trait("Integration-Search", "Sqlite")] +public class DataProviderSearchTest(SqliteTestProvider instance) : IntegrationSearchTests(instance); diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs new file mode 100644 index 00000000..a2866fe4 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/DataProvider/QueryBuilderTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Serilog.Ui.Core.Models; +using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Models; +using Xunit; + +namespace Sqlite.Tests.DataProvider; + +[Trait("Unit-QueryBuilder", "Sqlite")] +public class QueryBuilderTests +{ + [Theory] + [ClassData(typeof(QueryBuilderTestData))] + public void BuildFetchLogsQuery_ForSink_ReturnsCorrectQuery( + string schema, + string tableName, + string level, + string searchCriteria, + DateTime? startDate, + DateTime? endDate, + string expectedQuery) + { + // Arrange + Dictionary queryLogs = new() + { + ["level"] = level, + ["search"] = searchCriteria, + ["startDate"] = startDate?.ToString("O"), + ["endDate"] = endDate?.ToString("O") + }; + + SqliteSinkColumnNames sinkColumns = new(); + SqliteQueryBuilder sut = new(); + + // Act + string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs)); + + // Assert + query.Should().Be(expectedQuery); + } + + public class QueryBuilderTestData : IEnumerable + { + private readonly List _data = + [ + [ + string.Empty, "Logs", null!, null!, null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", null!, null!, null!, DateTime.Now, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Timestamp <= @EndDate ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", null!, null!, DateTime.Now, DateTime.Now, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Timestamp >= @StartDate AND Timestamp <= @EndDate ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", "Information", null!, null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Level = @Level ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", null!, "Test", null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE (RenderedMessage LIKE @Search OR Exception LIKE @Search) ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", "Information", "Test", null!, null!, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Level = @Level AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ], + [ + string.Empty, "Logs", "Information", "Test", DateTime.UtcNow, DateTime.UtcNow, + "SELECT Id, RenderedMessage AS Message, Level, Timestamp, Exception, Properties FROM Logs WHERE Level = @Level AND (RenderedMessage LIKE @Search OR Exception LIKE @Search) AND Timestamp >= @StartDate AND Timestamp <= @EndDate ORDER BY Timestamp DESC LIMIT @Offset, @Count" + ] + ]; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs new file mode 100644 index 00000000..3284bfc3 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Extensions/SerilogUiOptionBuilderExtensionsTest.cs @@ -0,0 +1,96 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.Core.Models.Options; +using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Extensions; +using Serilog.Ui.Web.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Sqlite.Tests.Extensions +{ + [Trait("DI-DataProvider", "Sqlite")] + public class SerilogUiOptionBuilderExtensionsTest + { + private readonly ServiceCollection serviceCollection; + + public SerilogUiOptionBuilderExtensionsTest() + { + serviceCollection = new ServiceCollection(); + } + + [Fact] + public void It_registers_provider_and_dependencies() + { + serviceCollection.AddSerilogUi(builder => + { + builder.UseSqliteServer(opt => opt.WithConnectionString("https://sqliteserver.example.com").WithTable("my-table")); + }); + var services = serviceCollection.BuildServiceProvider(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + + var provider = scope.ServiceProvider.GetService(); + provider.Should().NotBeNull().And.BeOfType(); + } + + [Fact] + public void It_registers_multiple_providers() + { + serviceCollection.AddSerilogUi(builder => + { + builder.UseSqliteServer(opt => opt.WithConnectionString("https://sqliteserver.example.com").WithTable("my-table")); + builder.UseSqliteServer(opt => opt.WithConnectionString("https://sqliteserver2.example.com").WithTable("my-table2")); + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + + var providers = scope.ServiceProvider.GetServices().ToList(); + providers.Should().HaveCount(2).And.AllBeOfType(); + providers.Select(p => p.Name).Should().OnlyHaveUniqueItems(); + + var providersOptions = serviceProvider.GetRequiredService(); + providersOptions.DisabledSortProviderNames.Should().BeEmpty(); + providersOptions.ExceptionAsStringProviderNames.Should().HaveCount(2); + } + + [Fact] + public void It_throws_on_invalid_registration() + { + var nullables = new List> + { + () => serviceCollection.AddSerilogUi((builder) => builder.UseSqliteServer(_ => {})), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString(null!).WithTable("my-table"))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString(" ").WithTable("my-table"))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString(string.Empty).WithTable("my-table"))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString("name").WithTable(null!))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString("name").WithTable(" "))), + () => serviceCollection.AddSerilogUi(builder => builder.UseSqliteServer(opt => + opt.WithConnectionString("name").WithTable(string.Empty))), + // if user sets an invalid schema, default value will be overridden an validation should fail + () => serviceCollection.AddSerilogUi(builder => + builder.UseSqliteServer(opt => opt.WithConnectionString("conn").WithTable("ok").WithSchema(null!))), + () => serviceCollection.AddSerilogUi(builder => + builder.UseSqliteServer(opt => opt.WithConnectionString("conn").WithTable("ok").WithSchema(" "))), + () => serviceCollection.AddSerilogUi(builder => + builder.UseSqliteServer(opt => opt.WithConnectionString("conn").WithTable("ok").WithSchema(string.Empty))), + }; + + foreach (var nullable in nullables) + { + nullable.Should().Throw(); + } + } + } +} diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj new file mode 100644 index 00000000..182a83e5 --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Serilog.Ui.SqliteProvider.Tests.csproj @@ -0,0 +1,38 @@ + + + + Sqlite.Tests + Sqlite.Tests + + + + + + + + + + + + + + + + + + + + + + %(Filename)%(Extension) + PreserveNewest + + + + + + + + + + diff --git a/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs new file mode 100644 index 00000000..7100e46c --- /dev/null +++ b/tests/Serilog.Ui.SqliteProvider.Tests/Util/SqliteTestProvider.cs @@ -0,0 +1,74 @@ +using Ardalis.GuardClauses; +using Dapper; +using Microsoft.Data.Sqlite; +using Serilog; +using Serilog.Ui.Common.Tests.DataSamples; +using Serilog.Ui.Common.Tests.TestSuites; +using Serilog.Ui.Core; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.SqliteDataProvider; +using Serilog.Ui.SqliteDataProvider.Extensions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Sqlite.Tests.Util +{ + [CollectionDefinition(nameof(SqliteTestProvider))] + public class SqliteCollection : ICollectionFixture { } + + public sealed class SqliteTestProvider : IIntegrationRunner + { + private static string DbName() => $"integration-{DateTime.UtcNow:O}.db".Replace(':', '-'); + private string _dbInstanceName = string.Empty; + + private LogModelPropsCollector? _collector; + + private SqliteDataProvider? _provider; + + public SqliteTestProvider() : base() + { + // No need to set up a container for SQLite - using in-memory database + } + + public SqliteDbOptions DbOptions { get; set; } = new SqliteDbOptions().WithTable("Logs"); + private async Task CheckDbReadinessAsync() + { + Guard.Against.Null(DbOptions); + + _dbInstanceName = DbName(); + DbOptions.WithConnectionString($"Data Source={_dbInstanceName}"); + + using var connection = new SqliteConnection(DbOptions.ConnectionString); + await connection.OpenAsync(); + + await connection.ExecuteAsync("SELECT 1"); + + InitializeAdditional(); + } + + private void InitializeAdditional() + { + var serilog = new SerilogSinkSetup(logger => + logger + .WriteTo + .SQLite(_dbInstanceName, storeTimestampInUtc: true)); + _collector = serilog.InitializeLogs(); + + _provider = new SqliteDataProvider(DbOptions, new SqliteQueryBuilder()); + } + + public IDataProvider GetDataProvider() => _provider!; + + public LogModelPropsCollector GetPropsCollector() => _collector!; + + public Task InitializeAsync() + { + return CheckDbReadinessAsync(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + public void Dispose() { } + } +}