├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── build-test-publish.yml ├── .gitignore ├── BenchmarkTests ├── BenchmarkTests.csproj ├── CacheCommandTests.cs ├── ConnectionParametersTests.cs ├── FormatStringTests.cs ├── HttpClientTests.cs ├── ParseStringsTests.cs └── Program.cs ├── LICENSE ├── NpgsqlRest.sln ├── NpgsqlRest ├── Auth │ ├── AuthHandler.cs │ └── ClaimsDictionary.cs ├── Consts.cs ├── Defaults │ ├── DefaultCommentParser.cs │ ├── DefaultEndpoint.cs │ ├── DefaultNameConverter.cs │ └── DefaultUrlBuilder.cs ├── Enums.cs ├── Extensions.cs ├── Formatter.cs ├── Interfaces.cs ├── Log.cs ├── NpgsqlRest.csproj ├── NpgsqlRestBatch.cs ├── NpgsqlRestCommand.cs ├── NpgsqlRestLogger.cs ├── NpgsqlRestMetadata.cs ├── NpgsqlRestMiddleware.cs ├── NpgsqlRestMiddlewareExtensions.cs ├── NpgsqlRestOptions.cs ├── NpgsqlRestParameter.cs ├── ParameterParser.cs ├── ParameterValidationValues.cs ├── Parser.cs ├── PasswordHasher.cs ├── PgConverters.cs ├── Routine.cs ├── RoutineCache.cs ├── RoutineEndpoint.cs ├── RoutineSource.cs ├── RoutineSourceParameterFormatter.cs ├── RoutineSourceQuery.cs ├── TypeDescriptor.cs └── UploadHandlers │ ├── CsvUploadHandler.cs │ ├── DefaultUploadHandler.cs │ ├── FileSystemUploadHandler.cs │ ├── IUploadHandler.cs │ ├── LargeObjectUploadHandler.cs │ ├── UploadExtensions.cs │ ├── UploadHandlerOptions.cs │ └── UploadLog.cs ├── NpgsqlRestClient ├── App.cs ├── AppStaticFileMiddleware.cs ├── Arguments.cs ├── Builder.cs ├── Config.cs ├── DbDataProtection.cs ├── DbLogging.cs ├── DefaultParser.cs ├── ExternalAuth.cs ├── NpgsqlRestClient.csproj ├── Program.cs ├── TokenRefreshAuth.cs └── appsettings.json ├── NpgsqlRestTests ├── ArrayTests │ ├── ArrayParametersTests.cs │ └── ArrayTypeTests.cs ├── AuthTests │ ├── AuthLoginTests.cs │ ├── AuthPasswordLoginTests.cs │ ├── AuthorizedTests.cs │ ├── HashedParameterTests.cs │ ├── PasswordHasherTests.cs │ ├── UserContextTests.cs │ └── UserParamsTests.cs ├── BodyTests │ ├── BodyParameterNameTests.cs │ └── BodyParamsTests.cs ├── CommandCallbackTests.cs ├── CommentTests │ ├── CommentAuthAttrTests.cs │ ├── CommentHttpAttrTests.cs │ ├── CommentParamTypeAttrTests.cs │ └── CommentResponseHeadersTests.cs ├── ConnectionNameTests │ ├── ConnectionNameTests.cs │ └── ConnectionNameUsingParamsTests.cs ├── CrudTests │ ├── CrudDeleteTests.cs │ ├── CrudInsertOnConflictDoNothingReturningTests.cs │ ├── CrudInsertOnConflictDoNothingTests.cs │ ├── CrudInsertOnConflictDoUpdateReturningTests.cs │ ├── CrudInsertOnConflictDoUpdateTests.cs │ ├── CrudInsertReturningTests.cs │ ├── CrudInsertTests.cs │ ├── CrudSelectTests.cs │ ├── CrudTableTagCommentTests.cs │ ├── CrudTableTests.cs │ ├── CrudUpdateReturningTests.cs │ └── CrudUpdateTests.cs ├── CustomSourceTests.cs ├── CustomTableTypeParametersTests.cs ├── CustomTypeParametersTests.cs ├── EndingSlashTests.cs ├── ErrorHandlingTests.cs ├── IdentNamesTests.cs ├── MixinTests.cs ├── NonPublicSchemaTests.cs ├── NotDotnetCompliantTypeParams.cs ├── NpgsqlRestTests.csproj ├── NullHandlingTests.cs ├── ParamTests │ ├── DefaultParametersTests.cs │ ├── MultiParamsQueryStringTests1.cs │ ├── MultiParamsQueryStringTests2.cs │ ├── MultiParamsTests1.cs │ ├── MultiParamsTests2.cs │ ├── NoneExistingParamTests.cs │ ├── OverloadQueryStringTests.cs │ ├── OverloadTests.cs │ ├── ReturnIntTests.cs │ ├── UnnamedParamsTests.cs │ └── VariadicParamTests.cs ├── ParserTests │ ├── DatabaseSerializerTests.cs │ ├── DefaultParserTests.cs │ ├── PatternMatcherTests.cs │ └── TimeSpanParserTests.cs ├── QuotedJsonTests.cs ├── RawContentTests │ ├── RawDownloadTests.cs │ ├── RawResponseTests.cs │ └── RawResponseUsingParamsTests.cs ├── RequestHeadersTests.cs ├── ReturnTests │ ├── ReturnBooleanTests.cs │ ├── ReturnJsonTests.cs │ ├── ReturnLongTableTests.cs │ ├── ReturnSetOfBoolTests.cs │ ├── ReturnSetOfIntTests.cs │ ├── ReturnSetOfJsonTests.cs │ ├── ReturnSetOfTextTests.cs │ ├── ReturnTableTests.cs │ ├── ReturnTableTypeTests.cs │ ├── ReturnTextTests.cs │ └── ReturnTypeTests.cs ├── SetofRecordTests.cs ├── SetofTableTests.cs ├── Setup │ ├── Database.cs │ ├── Program.cs │ ├── TestFixture.cs │ └── Usings.cs ├── SingleRecordTests.cs ├── StoredProcedureTests.cs ├── StrictFunctionsTests.cs ├── TimestampTests.cs ├── UploadTests │ ├── CsvExampleTests.cs │ ├── CsvUploadTests.cs │ ├── FileStatusCheckerTests.cs │ ├── FileSystemUploadTests.cs │ ├── LargeObjectUploadTests.cs │ └── MimeTypeFilterTests.cs ├── VoidUnitTests.cs └── VolatilityVerbTests.cs ├── PerfomanceTests ├── k6 │ └── script.js └── readme.md ├── README.md ├── annotations.md ├── changelog-client-old.md ├── changelog-old.md ├── changelog.md ├── client.md ├── docker ├── Dockerfile └── readme.md ├── login-endpoints.md ├── npm ├── config-copy.js ├── package.json ├── postinstall.js ├── readme.md └── uninstall.js ├── options.md └── plugins ├── NpgsqlRest.CrudSource ├── CrudSource.cs ├── CrudSourceQuery.cs ├── NpgsqlRest.CrudSource.csproj ├── ParameterFormatters.cs └── README.md ├── NpgsqlRest.HttpFiles ├── Enums.cs ├── HttpFile.cs ├── HttpFileOptions.cs ├── NpgsqlRest.HttpFiles.csproj └── README.md └── NpgsqlRest.TsClient ├── NpgsqlRest.TsClient.csproj ├── README.md ├── TsClient.cs └── TsClientOptions.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Default severity for analyzer diagnostics with category 'Naming' 4 | dotnet_analyzer_diagnostic.category-Naming.severity = silent 5 | csharp_using_directive_placement = outside_namespace:silent 6 | csharp_prefer_simple_using_statement = true:suggestion 7 | csharp_prefer_braces = true:silent 8 | csharp_style_namespace_declarations = block_scoped:silent 9 | csharp_style_prefer_method_group_conversion = true:silent 10 | csharp_style_prefer_top_level_statements = true:silent 11 | csharp_style_prefer_primary_constructors = true:suggestion 12 | csharp_style_expression_bodied_methods = false:silent 13 | csharp_style_expression_bodied_constructors = false:silent 14 | csharp_style_expression_bodied_operators = false:silent 15 | csharp_style_expression_bodied_properties = true:silent 16 | csharp_style_expression_bodied_indexers = true:silent 17 | csharp_style_expression_bodied_accessors = true:silent 18 | csharp_style_expression_bodied_lambdas = true:silent 19 | csharp_style_expression_bodied_local_functions = false:silent 20 | csharp_indent_labels = one_less_than_current 21 | csharp_space_around_binary_operators = before_and_after 22 | 23 | [*.{cs,vb}] 24 | #### Naming styles #### 25 | 26 | # Naming rules 27 | 28 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 29 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 30 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 31 | 32 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 33 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 34 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 35 | 36 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 37 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 38 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 39 | 40 | # Symbol specifications 41 | 42 | dotnet_naming_symbols.interface.applicable_kinds = interface 43 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 44 | dotnet_naming_symbols.interface.required_modifiers = 45 | 46 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 47 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 48 | dotnet_naming_symbols.types.required_modifiers = 49 | 50 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 51 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 52 | dotnet_naming_symbols.non_field_members.required_modifiers = 53 | 54 | # Naming styles 55 | 56 | dotnet_naming_style.begins_with_i.required_prefix = I 57 | dotnet_naming_style.begins_with_i.required_suffix = 58 | dotnet_naming_style.begins_with_i.word_separator = 59 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 60 | 61 | dotnet_naming_style.pascal_case.required_prefix = 62 | dotnet_naming_style.pascal_case.required_suffix = 63 | dotnet_naming_style.pascal_case.word_separator = 64 | dotnet_naming_style.pascal_case.capitalization = pascal_case 65 | 66 | dotnet_naming_style.pascal_case.required_prefix = 67 | dotnet_naming_style.pascal_case.required_suffix = 68 | dotnet_naming_style.pascal_case.word_separator = 69 | dotnet_naming_style.pascal_case.capitalization = pascal_case 70 | dotnet_style_coalesce_expression = true:suggestion 71 | dotnet_style_null_propagation = true:suggestion 72 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 73 | dotnet_style_prefer_auto_properties = true:silent 74 | dotnet_style_object_initializer = true:suggestion 75 | dotnet_style_prefer_collection_expression = true:suggestion 76 | dotnet_style_collection_initializer = true:suggestion 77 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 78 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 79 | dotnet_style_prefer_conditional_expression_over_return = true:silent 80 | dotnet_style_explicit_tuple_names = true:suggestion 81 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 82 | tab_width = 4 83 | indent_size = 4 84 | end_of_line = crlf 85 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 86 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: vbconsulting 4 | buy_me_a_coffee: vbilopavu 5 | -------------------------------------------------------------------------------- /.github/workflows/build-test-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test, Publish and Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-test-publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Install PosgtreSQL 15 | uses: vb-consulting/postgresql-action@v1 16 | with: 17 | postgresql version: '17' 18 | postgresql user: 'postgres' 19 | postgresql password: 'postgres' 20 | - uses: actions/checkout@v2 21 | - name: Setup .NET9 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: 9.0.x 25 | - name: Install dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --configuration Release --no-restore 29 | - name: Test 30 | run: dotnet test --no-restore --verbosity m 31 | - name: Publish 32 | run: dotnet nuget push **\*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{secrets.NUGET_API_KEY}} --skip-duplicate 33 | 34 | create-release: 35 | needs: build-test-publish 36 | runs-on: ubuntu-latest 37 | outputs: 38 | upload_url: ${{ steps.create_release.outputs.upload_url }} 39 | steps: 40 | - name: Create Release 41 | id: create_release 42 | uses: actions/create-release@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | tag_name: v2.27.0-client-v2.22.0 47 | release_name: "AOT Client v2.22.0 NpgsqlRest v2.27.0" 48 | draft: true 49 | prerelease: true 50 | 51 | upload-assets: 52 | needs: create-release 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v2 57 | 58 | - name: Upload appsettings.json to release 59 | uses: actions/upload-release-asset@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | upload_url: ${{ needs.create-release.outputs.upload_url }} 64 | asset_path: ./NpgsqlRestClient/appsettings.json 65 | asset_name: appsettings.json 66 | asset_content_type: application/json 67 | 68 | build-windows: 69 | needs: create-release 70 | runs-on: windows-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | - name: Setup .NET9 74 | uses: actions/setup-dotnet@v1 75 | with: 76 | dotnet-version: 9.0.x 77 | - name: Build Windows AOT 78 | run: dotnet publish ./NpgsqlRestClient/NpgsqlRestClient.csproj -r win-x64 -c Release 79 | - name: Upload Windows executable 80 | uses: actions/upload-release-asset@v1 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | with: 84 | upload_url: ${{ needs.create-release.outputs.upload_url }} 85 | asset_path: ./NpgsqlRestClient/bin/Release/net9.0/win-x64/publish/NpgsqlRestClient.exe 86 | asset_name: npgsqlrest-win64.exe 87 | asset_content_type: application/octet-stream 88 | 89 | build-linux: 90 | needs: create-release 91 | runs-on: ubuntu-22.04 92 | steps: 93 | - uses: actions/checkout@v2 94 | - name: Setup .NET9 95 | uses: actions/setup-dotnet@v1 96 | with: 97 | dotnet-version: 9.0.x 98 | - name: Build Linux AOT 99 | run: dotnet publish ./NpgsqlRestClient/NpgsqlRestClient.csproj -r linux-x64 -c Release 100 | - name: Upload Linux executable 101 | uses: actions/upload-release-asset@v1 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | with: 105 | upload_url: ${{ needs.create-release.outputs.upload_url }} 106 | asset_path: ./NpgsqlRestClient/bin/Release/net9.0/linux-x64/publish/NpgsqlRestClient 107 | asset_name: npgsqlrest-linux64 108 | asset_content_type: application/octet-stream 109 | 110 | build-macos: 111 | needs: create-release 112 | runs-on: macos-latest 113 | steps: 114 | - uses: actions/checkout@v2 115 | - name: Setup .NET9 116 | uses: actions/setup-dotnet@v1 117 | with: 118 | dotnet-version: 9.0.x 119 | - name: Build macOS AOT 120 | run: dotnet publish ./NpgsqlRestClient/NpgsqlRestClient.csproj -r osx-x64 -c Release 121 | - name: Upload macOS executable 122 | uses: actions/upload-release-asset@v1 123 | env: 124 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 125 | with: 126 | upload_url: ${{ needs.create-release.outputs.upload_url }} 127 | asset_path: ./NpgsqlRestClient/bin/Release/net9.0/osx-x64/publish/NpgsqlRestClient 128 | asset_name: npgsqlrest-osx64 129 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /BenchmarkTests/BenchmarkTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | True 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /BenchmarkTests/CacheCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using Npgsql; 4 | using NpgsqlRest; 5 | 6 | namespace BenchmarkTests; 7 | 8 | public class NpgsqlCachedCommand : NpgsqlCommand 9 | { 10 | private static readonly NpgsqlCachedCommand _instanceCache = new(); 11 | 12 | #pragma warning disable CS8603 // Possible null reference return. 13 | private NpgsqlCommand CachedCommandClone() => MemberwiseClone() as NpgsqlCommand; 14 | #pragma warning restore CS8603 // Possible null reference return. 15 | 16 | public static NpgsqlCommand Create(NpgsqlConnection connection) 17 | { 18 | var result = _instanceCache.CachedCommandClone(); 19 | result.Connection = connection; 20 | return result; 21 | } 22 | } 23 | 24 | public class NpgsqlCachedParameter : NpgsqlParameter 25 | { 26 | private static readonly NpgsqlCachedParameter _textParam = new() 27 | { 28 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 29 | Value = DBNull.Value, 30 | }; 31 | 32 | #pragma warning disable CS8603 // Possible null reference return. 33 | public NpgsqlParameter CachedParameterMemberwiseClone() => MemberwiseClone() as NpgsqlParameter; 34 | #pragma warning restore CS8603 // Possible null reference return. 35 | 36 | public static NpgsqlParameter CreateTextParam(string? value) 37 | { 38 | var result = _textParam.CachedParameterMemberwiseClone(); 39 | if (value is not null) 40 | { 41 | result.Value = value; 42 | } 43 | return result; 44 | } 45 | } 46 | 47 | [MemoryDiagnoser] 48 | public class CacheCommandTests 49 | { 50 | private readonly string _connectionStr = "Host=127.0.0.1;Port=5432;Database=postgres;Username=postgres;Password=postgres"; 51 | 52 | 53 | [Benchmark(Baseline = true)] 54 | public async Task NormalCommand() 55 | { 56 | using var connection = new NpgsqlConnection(_connectionStr); 57 | connection.Open(); 58 | using var cmd = connection.CreateCommand(); 59 | cmd.CommandText = "select $1,$2,$3"; 60 | cmd.Parameters.Add(new NpgsqlParameter 61 | { 62 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 63 | Value = "test1" 64 | }); 65 | cmd.Parameters.Add(new NpgsqlParameter 66 | { 67 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 68 | Value = "test2" 69 | }); 70 | cmd.Parameters.Add(new NpgsqlParameter 71 | { 72 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 73 | Value = "test3" 74 | }); 75 | await using var reader = await cmd.ExecuteReaderAsync(); 76 | } 77 | 78 | [Benchmark] 79 | public async Task CachedCommand() 80 | { 81 | using var connection = new NpgsqlConnection(_connectionStr); 82 | connection.Open(); 83 | using var cmd = NpgsqlCachedCommand.Create(connection); 84 | cmd.CommandText = "select $1,$2,$3"; 85 | cmd.Parameters.Add(new NpgsqlParameter 86 | { 87 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 88 | Value = "test1" 89 | }); 90 | cmd.Parameters.Add(new NpgsqlParameter 91 | { 92 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 93 | Value = "test2" 94 | }); 95 | cmd.Parameters.Add(new NpgsqlParameter 96 | { 97 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text, 98 | Value = "test3" 99 | }); 100 | await using var reader = await cmd.ExecuteReaderAsync(); 101 | } 102 | 103 | [Benchmark] 104 | public async Task CachedCommandAndParemeters() 105 | { 106 | using var connection = new NpgsqlConnection(_connectionStr); 107 | connection.Open(); 108 | using var cmd = NpgsqlCachedCommand.Create(connection); 109 | cmd.CommandText = "select $1,$2,$3"; 110 | cmd.Parameters.Add(NpgsqlCachedParameter.CreateTextParam("test1")); 111 | cmd.Parameters.Add(NpgsqlCachedParameter.CreateTextParam("test2")); 112 | cmd.Parameters.Add(NpgsqlCachedParameter.CreateTextParam("test3")); 113 | await using var reader = await cmd.ExecuteReaderAsync(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /BenchmarkTests/FormatStringTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | using BenchmarkDotNet.Attributes; 5 | using NpgsqlRest; 6 | 7 | namespace BenchmarkTests; 8 | 9 | public class FormatStringTests 10 | { 11 | private readonly Dictionary replacements = new() 12 | { 13 | { "name", "John" }, 14 | { "place", "NpgsqlRest" } 15 | }; 16 | 17 | [Benchmark] 18 | public ReadOnlySpan FormatStringMethod() 19 | { 20 | ReadOnlySpan input = "Hello, {name}! Welcome to {place}."; 21 | return Formatter.FormatString(input, replacements); 22 | } 23 | 24 | [Benchmark] 25 | public string RegexMethod() 26 | { 27 | ReadOnlySpan input = "Hello, {name}! Welcome to {place}."; 28 | string pattern = @"\{(\w+)\}"; 29 | return Regex.Replace(input.ToString(), pattern, match => 30 | { 31 | string key = match.Groups[1].Value; 32 | return replacements.TryGetValue(key, out var value) ? value : match.Value; 33 | }); 34 | } 35 | 36 | [Benchmark] 37 | public string RegexCodeGenMethod() 38 | { 39 | ReadOnlySpan input = "Hello, {name}! Welcome to {place}."; 40 | return ReplaceRegex.Value().Replace(input.ToString(), match => 41 | { 42 | string key = match.Groups[1].Value; 43 | return replacements.TryGetValue(key, out var value) ? value : match.Value; 44 | }); 45 | } 46 | } 47 | 48 | public static partial class ReplaceRegex 49 | { 50 | [GeneratedRegex(@"\{(\w+)\}", RegexOptions.Compiled)] 51 | public static partial Regex Value(); 52 | } -------------------------------------------------------------------------------- /BenchmarkTests/HttpClientTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using BenchmarkDotNet.Attributes; 4 | 5 | namespace BenchmarkTests; 6 | 7 | public class HttpClientTests 8 | { 9 | private HttpClient _client = null!; 10 | 11 | [GlobalSetup] 12 | public void Setup() 13 | { 14 | _client = new(); 15 | } 16 | 17 | [GlobalCleanup] 18 | public void Cleanup() 19 | { 20 | _client.Dispose(); 21 | } 22 | 23 | [Benchmark] 24 | public async Task CallPerfTests() 25 | { 26 | string json = """ 27 | { 28 | "records": 25, 29 | "textParam": "XYZ", 30 | "intParam": 3, 31 | "tsParam": "2024-04-04T03:03:03.00", 32 | "boolParam": false 33 | } 34 | """; 35 | using var content = new StringContent(json, Encoding.UTF8, "application/json"); 36 | using var result = await _client.PostAsync("http://localhost:5000/api/perf-test/", content); 37 | var response = await result.Content.ReadAsStringAsync(); 38 | } 39 | } -------------------------------------------------------------------------------- /BenchmarkTests/ParseStringsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using BenchmarkDotNet.Attributes; 4 | using NpgsqlRest; 5 | 6 | namespace BenchmarkTests; 7 | 8 | public class ParseStringsTests 9 | { 10 | readonly string[] names = ["test.txt", "x", "verylongfilename.txt", "abcde"]; 11 | readonly string[] patterns = ["*.txt", "*?*", "*long*.txt", "a?c*"]; 12 | 13 | [Benchmark] 14 | public void IsPatternMatchMethod() 15 | { 16 | foreach (var name in names) 17 | foreach (var pattern in patterns) 18 | { 19 | Parser.IsPatternMatch(name, pattern); 20 | } 21 | } 22 | 23 | //[Benchmark] 24 | //public void LikePatternIsMethod() 25 | //{ 26 | // foreach (var name in names) 27 | // foreach (var pattern in patterns) 28 | // { 29 | // DefaultResponseParser.LikePatternIsMatch(name, pattern); 30 | // } 31 | //} 32 | 33 | //[Benchmark] 34 | //public void LikePatternIsMatchFastMethod() 35 | //{ 36 | // foreach (var name in names) 37 | // foreach (var pattern in patterns) 38 | // { 39 | // DefaultResponseParser.LikePatternIsMatchFast(name, pattern); 40 | // } 41 | //} 42 | } 43 | -------------------------------------------------------------------------------- /BenchmarkTests/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Jobs; 3 | using BenchmarkDotNet.Loggers; 4 | using BenchmarkDotNet.Running; 5 | using BenchmarkDotNet.Toolchains.InProcess.Emit; 6 | using BenchmarkTests; 7 | using Perfolizer.Horology; 8 | 9 | BenchmarkRunner.Run(); 10 | 11 | //BenchmarkRunner 12 | // .Run( 13 | // DefaultConfig.Instance.AddJob( 14 | // Job.Default.WithToolchain(new InProcessEmitToolchain(timeout: TimeSpan.FromSeconds(9), logOutput: false)) 15 | // .WithLaunchCount(1) 16 | // .WithWarmupCount(5) 17 | // .WithIterationCount(100) 18 | // .WithIterationTime(TimeInterval.FromMilliseconds(80))) 19 | // .AddLogger(new ConsoleLogger(unicodeSupport: true, ConsoleLogger.CreateGrayScheme())) 20 | // .WithOptions(ConfigOptions.DisableLogFile)); 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023, 2024, 2025, 2026, 2027, 2028 VB-Consulting 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NpgsqlRest/Consts.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest; 2 | 3 | public static class Consts 4 | { 5 | public const string True = "true"; 6 | public const string False = "false"; 7 | public const string Null = "null"; 8 | public const char DoubleQuote = '"'; 9 | public const string DoubleQuoteColon = "\":"; 10 | public const char OpenParenthesis = '('; 11 | public const char CloseParenthesis = ')'; 12 | public const char Comma = ','; 13 | public const char Dot = '.'; 14 | public const char Backslash = '\\'; 15 | public const char Colon = ':'; 16 | public const char Equal = '='; 17 | public const char OpenBrace = '{'; 18 | public const char CloseBrace = '}'; 19 | public const char OpenBracket = '['; 20 | public const char CloseBracket = ']'; 21 | public const char Space = ' '; 22 | public const string DoubleColon = "::"; 23 | public const string FirstParam = "$1"; 24 | public const string FirstNamedParam = "=>$1"; 25 | public const string CloseParenthesisStr = ")"; 26 | public const char Dollar = '$'; 27 | public const string NamedParam = "=>$"; 28 | public const string OpenRow = "=>row("; 29 | public const string CloseRow = ")::"; 30 | public const char At = '@'; 31 | public const char Multiply = '*'; 32 | public const char Question = '?'; 33 | public const string EmptyArray = "[]"; 34 | public const string EmptyObj = "{}"; 35 | public const string SetContext = "select set_config($1,$2,false)"; 36 | } 37 | -------------------------------------------------------------------------------- /NpgsqlRest/Defaults/DefaultEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.Defaults; 2 | 3 | internal static class DefaultEndpoint 4 | { 5 | internal static RoutineEndpoint? Create( 6 | Routine routine, 7 | NpgsqlRestOptions options, 8 | ILogger? logger) 9 | { 10 | var url = options.UrlPathBuilder(routine, options); 11 | if (routine.FormatUrlPattern is not null) 12 | { 13 | url = string.Format(routine.FormatUrlPattern, url); 14 | } 15 | 16 | var method = routine.CrudType switch 17 | { 18 | CrudType.Select => Method.GET, 19 | CrudType.Update => Method.POST, 20 | CrudType.Insert => Method.PUT, 21 | CrudType.Delete => Method.DELETE, 22 | _ => Method.POST 23 | }; 24 | var requestParamType = method == Method.GET || method == Method.DELETE ? 25 | RequestParamType.QueryString : 26 | RequestParamType.BodyJson; 27 | 28 | RoutineEndpoint routineEndpoint = new( 29 | routine, 30 | url: url, 31 | method: method, 32 | requestParamType: requestParamType, 33 | requiresAuthorization: options.RequiresAuthorization, 34 | commandTimeout: options.CommandTimeout, 35 | responseContentType: null, 36 | responseHeaders: [], 37 | requestHeadersMode: options.RequestHeadersMode, 38 | requestHeadersParameterName: options.RequestHeadersParameterName, 39 | bodyParameterName: null, 40 | textResponseNullHandling: options.TextResponseNullHandling, 41 | queryStringNullHandling: options.QueryStringNullHandling, 42 | userContext: options.AuthenticationOptions.UseUserContext, 43 | userParameters: options.AuthenticationOptions.UseUserParameters); 44 | 45 | if (options.LogCommands && logger != null) 46 | { 47 | routineEndpoint.LogCallback = LoggerMessage.Define(LogLevel.Information, 48 | new EventId(5, nameof(routineEndpoint.LogCallback)), 49 | "{parameters}{command}", 50 | NpgsqlRestLogger.LogDefineOptions); 51 | } 52 | else 53 | { 54 | routineEndpoint.LogCallback = null; 55 | } 56 | 57 | if (routine.EndpointHandler is not null) 58 | { 59 | var parsed = DefaultCommentParser.Parse( 60 | routine, 61 | routineEndpoint, 62 | options, 63 | logger); 64 | 65 | return routine.EndpointHandler(parsed); 66 | } 67 | 68 | return DefaultCommentParser.Parse( 69 | routine, 70 | routineEndpoint, 71 | options, 72 | logger); 73 | } 74 | } -------------------------------------------------------------------------------- /NpgsqlRest/Defaults/DefaultNameConverter.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.Defaults; 2 | 3 | public static class DefaultNameConverter 4 | { 5 | private static readonly string[] separator = ["_"]; 6 | 7 | public static string? ConvertToCamelCase(string? value) 8 | { 9 | if (value is null) 10 | { 11 | return string.Empty; 12 | } 13 | if (value.Length == 0) 14 | { 15 | return string.Empty; 16 | } 17 | return value 18 | .Split(separator, StringSplitOptions.RemoveEmptyEntries) 19 | .Select((s, i) => 20 | string.Concat(i == 0 ? char.ToLowerInvariant(s[0]) : char.ToUpperInvariant(s[0]), s[1..])) 21 | .Aggregate(string.Empty, string.Concat) 22 | .Trim(Consts.DoubleQuote); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NpgsqlRest/Defaults/DefaultUrlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.Defaults; 2 | 3 | public static class DefaultUrlBuilder 4 | { 5 | public static string CreateUrl(Routine routine, NpgsqlRestOptions options) 6 | { 7 | var schema = routine.Schema.ToLowerInvariant() 8 | .Replace("_", "-") 9 | .Replace(" ", "-") 10 | .Replace("\"", "") 11 | .Trim('/'); 12 | 13 | schema = string.IsNullOrEmpty(schema) || string.Equals(schema, "public", StringComparison.OrdinalIgnoreCase) ? "" : string.Concat(schema, "/"); 14 | var name = routine.Name.ToLowerInvariant() 15 | .Replace("_", "-") 16 | .Replace(" ", "-") 17 | .Replace("\"", "") 18 | .Trim('/'); 19 | 20 | var prefix = options.UrlPathPrefix is null ? "/" : 21 | string.Concat("/", options.UrlPathPrefix 22 | .ToLowerInvariant() 23 | .Replace("_", "-") 24 | .Replace(" ", "-") 25 | .Replace("\"", "") 26 | .Trim('/'), 27 | "/"); 28 | 29 | return string.Concat(prefix, schema, name).TrimEnd('/').Trim(Consts.DoubleQuote); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /NpgsqlRest/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest; 2 | 3 | public enum RoutineType { Table, View, Function, Procedure, Other } 4 | public enum CrudType { Select, Insert, Update, Delete } 5 | public enum Method { GET, PUT, POST, DELETE, HEAD, OPTIONS, TRACE, PATCH, CONNECT } 6 | public enum RequestParamType { QueryString, BodyJson } 7 | public enum ParamType { QueryString, BodyJson, BodyParam, HeaderParam } 8 | public enum CommentHeader { None, Simple, Full } 9 | public enum TextResponseNullHandling { EmptyString, NullLiteral, NoContent } 10 | public enum QueryStringNullHandling { EmptyString, NullLiteral, Ignore } 11 | 12 | public enum ServiceProviderObject 13 | { 14 | /// 15 | /// Connection is not provided in service provider. Connection is supplied either by ConnectionString or by DataSource option. 16 | /// 17 | None, 18 | /// 19 | /// NpgsqlRest attempts to get NpgsqlDataSource from service provider (assuming one is provided). 20 | /// 21 | NpgsqlDataSource, 22 | /// 23 | /// NpgsqlRest attempts to get NpgsqlConnection from service provider (assuming one is provided). 24 | /// 25 | NpgsqlConnection 26 | } 27 | 28 | public enum CommentsMode 29 | { 30 | /// 31 | /// Routine comments are ignored. 32 | /// 33 | Ignore, 34 | /// 35 | /// Creates all endpoints and parses comments for to configure endpoint meta data. 36 | /// 37 | ParseAll, 38 | /// 39 | /// Creates only endpoints from routines containing a comment with HTTP tag and and configures endpoint meta data. 40 | /// 41 | OnlyWithHttpTag 42 | } 43 | 44 | public enum RequestHeadersMode 45 | { 46 | /// 47 | /// Ignore request headers, don't send them to PostgreSQL (default). 48 | /// 49 | Ignore, 50 | /// 51 | /// Send all request headers as json object to PostgreSQL by executing set_config('context.headers', headers, false) before routine call. 52 | /// 53 | Context, 54 | /// 55 | /// Send all request headers as json object to PostgreSQL as default routine parameter with name set by RequestHeadersParameterName option. 56 | /// This parameter has to have the default value (null) in the routine and have to be text or json type. 57 | /// 58 | Parameter 59 | } 60 | 61 | public enum PostgresConnectionNoticeLoggingMode 62 | { 63 | /// 64 | /// Log only connection messages. 65 | /// 66 | MessageOnly, 67 | /// 68 | /// Log last stack trace and message. 69 | /// 70 | FirstStackFrameAndMessage, 71 | /// 72 | /// Log full stack trace and message. 73 | /// 74 | FullStackAndMessage 75 | } -------------------------------------------------------------------------------- /NpgsqlRest/Formatter.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest; 2 | 3 | public static class Formatter 4 | { 5 | public static ReadOnlySpan FormatString(ReadOnlySpan input, Dictionary replacements) 6 | { 7 | if (replacements is null || replacements.Count == 0) 8 | { 9 | return input; 10 | } 11 | 12 | int inputLength = input.Length; 13 | 14 | if (inputLength == 0) 15 | { 16 | return input; 17 | } 18 | 19 | return FormatString(input, replacements.GetAlternateLookup>()); 20 | } 21 | 22 | public static ReadOnlySpan FormatString(ReadOnlySpan input, Dictionary.AlternateLookup> lookup) 23 | { 24 | int inputLength = input.Length; 25 | 26 | if (inputLength == 0) 27 | { 28 | return input; 29 | } 30 | 31 | int resultLength = 0; 32 | bool inside = false; 33 | int startIndex = 0; 34 | for (int i = 0; i < inputLength; i++) 35 | { 36 | var ch = input[i]; 37 | if (ch == Consts.OpenBrace) 38 | { 39 | if (inside is true) 40 | { 41 | resultLength += input[startIndex..i].Length; 42 | } 43 | inside = true; 44 | startIndex = i; 45 | continue; 46 | } 47 | 48 | if (ch == Consts.CloseBrace && inside is true) 49 | { 50 | inside = false; 51 | if (lookup.TryGetValue(input[(startIndex + 1)..i], out var value)) 52 | { 53 | resultLength += value.Length; 54 | } 55 | else 56 | { 57 | resultLength += input[startIndex..i].Length + 1; 58 | } 59 | continue; 60 | } 61 | 62 | if (inside is false) 63 | { 64 | resultLength += 1; 65 | } 66 | } 67 | if (inside is true) 68 | { 69 | resultLength += input[startIndex..].Length; 70 | } 71 | 72 | char[] resultArray = new char[resultLength]; 73 | Span result = resultArray; 74 | int resultPos = 0; 75 | 76 | inside = false; 77 | startIndex = 0; 78 | for (int i = 0; i < inputLength; i++) 79 | { 80 | var ch = input[i]; 81 | if (ch == Consts.OpenBrace) 82 | { 83 | if (inside is true) 84 | { 85 | input[startIndex..i].CopyTo(result[resultPos..]); 86 | resultPos += input[startIndex..i].Length; 87 | } 88 | inside = true; 89 | startIndex = i; 90 | continue; 91 | } 92 | 93 | if (ch == Consts.CloseBrace && inside is true) 94 | { 95 | inside = false; 96 | if (lookup.TryGetValue(input[(startIndex + 1)..i], out var value)) 97 | { 98 | value.AsSpan().CopyTo(result[resultPos..]); 99 | resultPos += value.Length; 100 | } 101 | else 102 | { 103 | input[startIndex..i].CopyTo(result[resultPos..]); 104 | resultPos += input[startIndex..i].Length; 105 | result[resultPos] = ch; 106 | resultPos++; 107 | } 108 | continue; 109 | } 110 | 111 | if (inside is false) 112 | { 113 | result[resultPos] = ch; 114 | resultPos++; 115 | } 116 | } 117 | if (inside is true) 118 | { 119 | input[startIndex..].CopyTo(result[resultPos..]); 120 | } 121 | 122 | return resultArray; 123 | } 124 | } -------------------------------------------------------------------------------- /NpgsqlRest/NpgsqlRest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net9.0 6 | 13.0 7 | enable 8 | enable 9 | true 10 | $(NoWarn);1591 11 | true 12 | NpgsqlRest 13 | VB-Consulting 14 | VB-Consulting 15 | VB-Consulting 16 | Automatic REST API for Any Postgres Database as .NET9 Middleware 17 | api;api-rest;restful-api;http;postgres;dotnet;database;rest;server;postgresql;npgsqlrest;pgsql;pg;automatic 18 | https://github.com/vb-consulting/NpgsqlRest 19 | https://github.com/vb-consulting/NpgsqlRest 20 | https://github.com/vb-consulting/NpgsqlRest/blob/master/changelog.md 21 | NpgsqlRest 22 | LICENSE 23 | true 24 | true 25 | snupkg 26 | true 27 | true 28 | true 29 | true 30 | README.MD 31 | bin\$(Configuration)\$(AssemblyName).xml 32 | 2.27.0 33 | 2.27.0 34 | 2.27.0 35 | 2.27.0 36 | 37 | 38 | 39 | true 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | True 51 | 52 | 53 | 54 | 55 | 56 | 57 | True 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /NpgsqlRest/NpgsqlRestBatch.cs: -------------------------------------------------------------------------------- 1 | using Npgsql; 2 | 3 | namespace NpgsqlRest; 4 | 5 | public class NpgsqlRestBatch : NpgsqlBatch 6 | { 7 | private NpgsqlBatch NpgsqlBatchClone() 8 | { 9 | #pragma warning disable CS8603 // Possible null reference return. 10 | return MemberwiseClone() as NpgsqlBatch; 11 | #pragma warning restore CS8603 // Possible null reference return. 12 | } 13 | 14 | private static readonly NpgsqlRestBatch _instanceCache = new(); 15 | 16 | public static NpgsqlBatch Create(NpgsqlConnection connection) 17 | { 18 | var result = _instanceCache.NpgsqlBatchClone(); 19 | result.Connection = connection; 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /NpgsqlRest/NpgsqlRestCommand.cs: -------------------------------------------------------------------------------- 1 | using Npgsql; 2 | 3 | namespace NpgsqlRest; 4 | 5 | public class NpgsqlRestCommand : NpgsqlCommand 6 | { 7 | private NpgsqlCommand NpgsqlCommandClone() 8 | { 9 | #pragma warning disable CS8603 // Possible null reference return. 10 | return MemberwiseClone() as NpgsqlCommand; 11 | #pragma warning restore CS8603 // Possible null reference return. 12 | } 13 | 14 | private static readonly NpgsqlRestCommand _instanceCache = new(); 15 | 16 | public static NpgsqlCommand Create(NpgsqlConnection connection) 17 | { 18 | var result = _instanceCache.NpgsqlCommandClone(); 19 | result.Connection = connection; 20 | return result; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NpgsqlRest/NpgsqlRestMiddlewareExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Net; 3 | using static System.Net.Mime.MediaTypeNames; 4 | 5 | namespace NpgsqlRest; 6 | 7 | public static class NpgsqlRestMiddlewareExtensions 8 | { 9 | public static IApplicationBuilder UseNpgsqlRest(this IApplicationBuilder builder, NpgsqlRestOptions options) 10 | { 11 | if (options.ConnectionString is null && options.DataSource is null && options.ServiceProviderMode == ServiceProviderObject.None) 12 | { 13 | throw new ArgumentException("ConnectionString and DataSource are null and ServiceProviderMode is set to None. You must specify connection with connection string, DataSource object or with ServiceProvider"); 14 | } 15 | 16 | if (options.ConnectionString is not null && options.DataSource is not null && options.ServiceProviderMode == ServiceProviderObject.None) 17 | { 18 | throw new ArgumentException("Both ConnectionString and DataSource are provided. Please specify only one."); 19 | } 20 | 21 | if (options.Logger is not null) 22 | { 23 | NpgsqlRestMiddleware.SetLogger(options.Logger); 24 | } 25 | else if (builder is WebApplication app) 26 | { 27 | var factory = app.Services.GetRequiredService(); 28 | NpgsqlRestMiddleware.SetLogger(factory is not null ? factory.CreateLogger(options.LoggerName ?? typeof(NpgsqlRestMiddlewareExtensions).Namespace ?? "NpgsqlRest") : app.Logger); 29 | } 30 | 31 | 32 | NpgsqlRestMiddleware.SetMetadata(NpgsqlRestMetadataBuilder.Build(options, NpgsqlRestMiddleware.Logger, builder)); 33 | if (NpgsqlRestMiddleware.Metadata.Entries.Count == 0) 34 | { 35 | return builder; 36 | } 37 | NpgsqlRestMiddleware.SetOptions(options); 38 | NpgsqlRestMiddleware.SetServiceProvider(builder.ApplicationServices); 39 | 40 | if (options.RefreshEndpointEnabled) 41 | { 42 | var refreshMethodUpper = options.RefreshMethod.ToUpperInvariant(); 43 | var refreshPathUpper = options.RefreshPath.ToUpperInvariant(); 44 | 45 | builder.Use(async (context, next) => 46 | { 47 | if (context.Request.Method.Equals(refreshMethodUpper, StringComparison.OrdinalIgnoreCase) && 48 | context.Request.Path.Equals(refreshPathUpper, StringComparison.OrdinalIgnoreCase)) 49 | { 50 | try 51 | { 52 | Volatile.Write(ref NpgsqlRestMiddleware.metadata, NpgsqlRestMetadataBuilder.Build(options, options.Logger, builder)); 53 | NpgsqlRestMiddleware.lookup = NpgsqlRestMiddleware.metadata.Entries.GetAlternateLookup>(); 54 | context.Response.StatusCode = (int)HttpStatusCode.OK; 55 | await context.Response.CompleteAsync(); 56 | } 57 | catch (Exception e) 58 | { 59 | options.Logger?.LogError(e, "Failed to refresh metadata"); 60 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 61 | context.Response.ContentType = Text.Plain; 62 | await context.Response.WriteAsync($"Failed to refresh metadata: {e.Message}"); 63 | await context.Response.CompleteAsync(); 64 | } 65 | return; 66 | } 67 | await next(context); 68 | }); 69 | } 70 | 71 | return builder.UseMiddleware(); 72 | } 73 | } -------------------------------------------------------------------------------- /NpgsqlRest/ParameterValidationValues.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest; 2 | 3 | public class ParameterValidationValues( 4 | HttpContext context, 5 | Routine routine, 6 | NpgsqlRestParameter parameter) 7 | { 8 | /// 9 | /// Current HttpContext. 10 | /// 11 | public readonly HttpContext Context = context; 12 | /// 13 | /// Current Routine. 14 | /// 15 | public readonly Routine Routine = routine; 16 | /// 17 | /// Parameter to be validated. Note: if parameter is using default value and value not provided, parameter.Value is null. 18 | /// 19 | public readonly NpgsqlRestParameter Parameter = parameter; 20 | } 21 | -------------------------------------------------------------------------------- /NpgsqlRest/Parser.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace NpgsqlRest; 5 | 6 | public static partial class Parser 7 | { 8 | [GeneratedRegex(@"^(\d*\.?\d+)\s*([a-z]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase)] 9 | private static partial Regex IntervalRegex(); 10 | 11 | public static TimeSpan? ParsePostgresInterval(string interval) 12 | { 13 | if (string.IsNullOrWhiteSpace(interval)) 14 | { 15 | return null; 16 | } 17 | 18 | interval = interval.Trim().ToLowerInvariant(); 19 | 20 | // Match number (integer or decimal) followed by optional space and unit 21 | var match = IntervalRegex().Match(interval); 22 | if (!match.Success) 23 | { 24 | return null; 25 | } 26 | 27 | string numberPart = match.Groups[1].Value; 28 | string unitPart = match.Groups[2].Value; 29 | 30 | if (!double.TryParse(numberPart, System.Globalization.NumberStyles.Any, 31 | System.Globalization.CultureInfo.InvariantCulture, out double value)) 32 | { 33 | return null; 34 | } 35 | 36 | // Map PostgreSQL units to TimeSpan conversions 37 | return unitPart switch 38 | { 39 | "s" or "sec" or "second" or "seconds" => TimeSpan.FromSeconds(value), 40 | "m" or "min" or "minute" or "minutes" => TimeSpan.FromMinutes(value), 41 | "h" or "hour" or "hours" => TimeSpan.FromHours(value), 42 | "d" or "day" or "days" => TimeSpan.FromDays(value), 43 | _ => null 44 | }; 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static bool IsPatternMatch(string name, string pattern) 49 | { 50 | if (name == null || pattern == null) return false; 51 | int nl = name.Length, pl = pattern.Length; 52 | if (nl == 0 || pl == 0) return false; 53 | 54 | if (pl > 1 && pattern[0] == Consts.Multiply && pattern[1] == Consts.Dot) 55 | { 56 | ReadOnlySpan ext = pattern.AsSpan(1); 57 | return nl > ext.Length && name.AsSpan(nl - ext.Length).Equals(ext, StringComparison.OrdinalIgnoreCase); 58 | } 59 | 60 | int ni = 0, pi = 0; 61 | int lastStar = -1, lastMatch = 0; 62 | 63 | while (ni < nl) 64 | { 65 | if (pi < pl) 66 | { 67 | char pc = pattern[pi]; 68 | if (pc == Consts.Multiply) 69 | { 70 | lastStar = pi++; 71 | lastMatch = ni; 72 | continue; 73 | } 74 | if (pc == Consts.Question ? ni < nl : char.ToLowerInvariant(pc) == char.ToLowerInvariant(name[ni])) 75 | { 76 | ni++; 77 | pi++; 78 | continue; 79 | } 80 | } 81 | if (lastStar >= 0) 82 | { 83 | pi = lastStar + 1; 84 | ni = ++lastMatch; 85 | continue; 86 | } 87 | return false; 88 | } 89 | 90 | while (pi < pl && pattern[pi] == Consts.Multiply) pi++; 91 | return pi == pl; 92 | } 93 | } -------------------------------------------------------------------------------- /NpgsqlRest/PasswordHasher.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace NpgsqlRest; 4 | 5 | public interface IPasswordHasher 6 | { 7 | /// 8 | /// Hashes a password. 9 | /// 10 | /// The password to hash. 11 | /// A hashed representation of the password. 12 | string HashPassword(string password); 13 | 14 | /// 15 | /// Verifies a provided password against a stored hash. 16 | /// 17 | /// The stored hashed password. 18 | /// The password to verify. 19 | /// True if the password matches, false otherwise. 20 | bool VerifyHashedPassword(string hashedPassword, string providedPassword); 21 | } 22 | 23 | public class PasswordHasher : IPasswordHasher 24 | { 25 | // Configuration constants 26 | private const int SaltByteSize = 16; // 128-bit salt 27 | private const int HashByteSize = 32; // 256-bit hash 28 | private const int Iterations = 600_000; // OWASP-recommended iteration count for PBKDF2-SHA256 (2025) 29 | 30 | /// 31 | /// Hashes a password. 32 | /// 33 | /// The password to hash. 34 | /// A hashed representation of the password. 35 | /// Thrown if the password is null or empty. 36 | public string HashPassword(string password) 37 | { 38 | if (string.IsNullOrEmpty(password)) 39 | throw new ArgumentException("Password cannot be null or empty.", nameof(password)); 40 | 41 | // Generate a random salt 42 | byte[] salt = RandomNumberGenerator.GetBytes(SaltByteSize); 43 | 44 | // Hash the password using PBKDF2 45 | using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256); 46 | byte[] hash = pbkdf2.GetBytes(HashByteSize); 47 | 48 | // Combine salt and hash into a single byte array 49 | byte[] hashBytes = new byte[SaltByteSize + HashByteSize]; 50 | Array.Copy(salt, 0, hashBytes, 0, SaltByteSize); 51 | Array.Copy(hash, 0, hashBytes, SaltByteSize, HashByteSize); 52 | 53 | // Convert to base64 for storage 54 | return Convert.ToBase64String(hashBytes); 55 | } 56 | 57 | /// 58 | /// Verifies a provided password against a stored hash. 59 | /// 60 | /// The stored hashed password. 61 | /// The password to verify. 62 | /// True if the password matches, false otherwise. 63 | public bool VerifyHashedPassword(string hashedPassword, string providedPassword) 64 | { 65 | if (string.IsNullOrEmpty(providedPassword) || string.IsNullOrEmpty(hashedPassword)) 66 | return false; 67 | 68 | try 69 | { 70 | // Decode the stored hash 71 | byte[] hashBytes = Convert.FromBase64String(hashedPassword); 72 | 73 | // Validate the stored hash length 74 | if (hashBytes.Length != SaltByteSize + HashByteSize) 75 | return false; 76 | 77 | // Extract the salt 78 | byte[] salt = new byte[SaltByteSize]; 79 | Array.Copy(hashBytes, 0, salt, 0, SaltByteSize); 80 | 81 | // Extract the hash 82 | byte[] storedSubHash = new byte[HashByteSize]; 83 | Array.Copy(hashBytes, SaltByteSize, storedSubHash, 0, HashByteSize); 84 | 85 | // Compute the hash of the provided password 86 | using var pbkdf2 = new Rfc2898DeriveBytes(providedPassword, salt, Iterations, HashAlgorithmName.SHA256); 87 | byte[] computedHash = pbkdf2.GetBytes(HashByteSize); 88 | 89 | // Compare the hashes in constant time 90 | return CryptographicOperations.FixedTimeEquals(computedHash, storedSubHash); 91 | } 92 | catch 93 | { 94 | // Handle invalid base64 or other errors 95 | return false; 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /NpgsqlRest/RoutineCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace NpgsqlRest; 4 | 5 | public interface IRoutineCache 6 | { 7 | bool Get(RoutineEndpoint endpoint, string key, out object? result); 8 | void AddOrUpdate(RoutineEndpoint endpoint, string key, object? value); 9 | } 10 | 11 | public class RoutineCache : IRoutineCache 12 | { 13 | private class CacheEntry 14 | { 15 | public object? Value { get; set; } 16 | public DateTime? ExpirationTime { get; set; } 17 | public bool IsExpired => ExpirationTime.HasValue && DateTime.UtcNow > ExpirationTime.Value; 18 | } 19 | 20 | private static readonly ConcurrentDictionary _cache = new(); 21 | private static readonly ConcurrentDictionary _originalKeys = new(); 22 | private static Timer? _cleanupTimer; 23 | 24 | public static void Start(NpgsqlRestOptions options) 25 | { 26 | _cleanupTimer = new Timer( 27 | _ => CleanupExpiredEntriesInternal(), 28 | null, 29 | TimeSpan.FromMinutes(options.CachePruneIntervalMin), 30 | TimeSpan.FromMinutes(options.CachePruneIntervalMin)); 31 | } 32 | 33 | public static void Shutdown() 34 | { 35 | _cleanupTimer?.Dispose(); 36 | _cache.Clear(); 37 | _originalKeys.Clear(); 38 | } 39 | 40 | private static void CleanupExpiredEntriesInternal() 41 | { 42 | var expiredKeys = _cache 43 | .Where(kvp => kvp.Value.IsExpired) 44 | .Select(kvp => kvp.Key) 45 | .ToList(); 46 | 47 | foreach (var key in expiredKeys) 48 | { 49 | _cache.TryRemove(key, out _); 50 | _originalKeys.TryRemove(key, out _); 51 | } 52 | } 53 | 54 | public bool Get(RoutineEndpoint endpoint, string key, out object? result) 55 | { 56 | var hashedKey = key.GetHashCode(); 57 | 58 | if (_cache.TryGetValue(hashedKey, out var entry)) 59 | { 60 | if (_originalKeys.TryGetValue(hashedKey, out var originalKey) && originalKey == key) 61 | { 62 | if (entry.IsExpired) 63 | { 64 | // Remove expired entry 65 | _cache.TryRemove(hashedKey, out _); 66 | _originalKeys.TryRemove(hashedKey, out _); 67 | result = null; 68 | return false; 69 | } 70 | 71 | result = entry.Value; 72 | return true; 73 | } 74 | } 75 | 76 | result = null; 77 | return false; 78 | } 79 | 80 | public void AddOrUpdate(RoutineEndpoint endpoint, string key, object? value) 81 | { 82 | var hashedKey = key.GetHashCode(); 83 | var entry = new CacheEntry 84 | { 85 | Value = value, 86 | ExpirationTime = endpoint.CacheExpiresIn.HasValue ? DateTime.UtcNow + endpoint.CacheExpiresIn.Value : null 87 | }; 88 | 89 | _cache[hashedKey] = entry; 90 | _originalKeys[hashedKey] = key; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /NpgsqlRest/RoutineEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Primitives; 2 | 3 | namespace NpgsqlRest; 4 | 5 | public class RoutineEndpoint( 6 | Routine routine, 7 | string url, 8 | Method method, 9 | RequestParamType requestParamType, 10 | bool requiresAuthorization, 11 | int? commandTimeout, 12 | string? responseContentType, 13 | Dictionary responseHeaders, 14 | RequestHeadersMode requestHeadersMode, 15 | string requestHeadersParameterName, 16 | string? bodyParameterName, 17 | TextResponseNullHandling textResponseNullHandling, 18 | QueryStringNullHandling queryStringNullHandling, 19 | HashSet? authorizeRoles = null, 20 | bool login = false, 21 | bool logout = false, 22 | bool securitySensitive = false, 23 | ulong? bufferRows = null, 24 | bool raw = false, 25 | string? rawValueSeparator = null, 26 | string? rawNewLineSeparator = null, 27 | bool rawColumnNames = false, 28 | bool cached = false, 29 | string[]? cachedParams = null, 30 | TimeSpan? cacheExpiresIn = null, 31 | bool parseResponse = false, 32 | string? connectionName = null, 33 | bool upload = false, 34 | string[]? uploadHandlers = null, 35 | Dictionary? customParameters = null, 36 | bool userContext = false, 37 | bool userParameters = false) 38 | { 39 | private string? _bodyParameterName = bodyParameterName; 40 | 41 | internal bool HasBodyParameter = !string.IsNullOrWhiteSpace(bodyParameterName); 42 | internal Action? LogCallback { get; set; } 43 | internal bool HeadersNeedParsing { get; set; } = false; 44 | internal bool CustomParamsNeedParsing { get; set; } = false; 45 | 46 | public Routine Routine { get; } = routine; 47 | public string Url { get; set; } = url; 48 | public Method Method { get; set; } = method; 49 | public RequestParamType RequestParamType { get; set; } = requestParamType; 50 | public bool RequiresAuthorization { get; set; } = requiresAuthorization; 51 | public int? CommandTimeout { get; set; } = commandTimeout; 52 | public string? ResponseContentType { get; set; } = responseContentType; 53 | public Dictionary ResponseHeaders { get; set; } = responseHeaders; 54 | public RequestHeadersMode RequestHeadersMode { get; set; } = requestHeadersMode; 55 | public string RequestHeadersParameterName { get; set; } = requestHeadersParameterName; 56 | public string? BodyParameterName 57 | { 58 | get => _bodyParameterName; 59 | set 60 | { 61 | HasBodyParameter = !string.IsNullOrWhiteSpace(value); 62 | _bodyParameterName = value; 63 | } 64 | } 65 | public TextResponseNullHandling TextResponseNullHandling { get; set; } = textResponseNullHandling; 66 | public QueryStringNullHandling QueryStringNullHandling { get; set; } = queryStringNullHandling; 67 | public HashSet? AuthorizeRoles { get; set; } = authorizeRoles; 68 | public bool Login { get; set; } = login; 69 | public bool Logout { get; set; } = logout; 70 | public bool SecuritySensitive { get; set; } = securitySensitive; 71 | public bool IsAuth => Login || Logout || SecuritySensitive; 72 | public ulong? BufferRows { get; set; } = bufferRows; 73 | public bool Raw { get; set; } = raw; 74 | public string? RawValueSeparator { get; set; } = rawValueSeparator; 75 | public string? RawNewLineSeparator { get; set; } = rawNewLineSeparator; 76 | public bool RawColumnNames { get; set; } = rawColumnNames; 77 | public string[][]? CommentWordLines { get; internal set; } 78 | public bool Cached { get; set; } = cached; 79 | public HashSet? CachedParams { get; set; } = cachedParams?.ToHashSet(); 80 | public TimeSpan? CacheExpiresIn { get; set; } = cacheExpiresIn; 81 | public bool ParseResponse { get; set; } = parseResponse; 82 | public string? ConnectionName { get; set; } = connectionName; 83 | public bool Upload { get; set; } = upload; 84 | public string[]? UploadHandlers { get; set; } = uploadHandlers; 85 | public Dictionary? CustomParameters { get; set; } = customParameters; 86 | public bool UserContext { get; set; } = userContext; 87 | public bool UseUserParameters { get; set; } = userParameters; 88 | } 89 | -------------------------------------------------------------------------------- /NpgsqlRest/RoutineSourceParameterFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text; 3 | using Npgsql; 4 | 5 | namespace NpgsqlRest; 6 | 7 | public class RoutineSourceParameterFormatter : IRoutineSourceParameterFormatter 8 | { 9 | public bool IsFormattable { get; } = false; 10 | 11 | public string AppendCommandParameter(NpgsqlRestParameter parameter, int index) 12 | { 13 | var suffix = parameter.TypeDescriptor.IsCastToText() ? 14 | string.Concat(Consts.DoubleColon, parameter.TypeDescriptor.OriginalType) : 15 | string.Empty; 16 | 17 | if (index == 0) 18 | { 19 | return parameter.ActualName is null ? 20 | string.Concat(Consts.FirstParam, suffix) : 21 | string.Concat(parameter.ActualName, Consts.FirstNamedParam, suffix); 22 | } 23 | 24 | var indexStr = (index + 1).ToString(CultureInfo.InvariantCulture); 25 | 26 | return parameter.ActualName is null ? 27 | string.Concat(Consts.Comma, Consts.Dollar, indexStr, suffix) : 28 | string.Concat(Consts.Comma, parameter.ActualName, Consts.NamedParam, indexStr, suffix); 29 | } 30 | 31 | public string? AppendEmpty() => Consts.CloseParenthesisStr; 32 | } 33 | 34 | public class RoutineSourceCustomTypesParameterFormatter : IRoutineSourceParameterFormatter 35 | { 36 | public bool IsFormattable { get; } = true; 37 | 38 | public string FormatCommand(Routine routine, NpgsqlParameterCollection parameters) 39 | { 40 | var sb = new StringBuilder(routine.Expression, routine.Expression.Length + parameters.Count * 20); 41 | var count = parameters.Count; 42 | 43 | var culture = CultureInfo.InvariantCulture; 44 | 45 | for (var i = 0; i < count; i++) 46 | { 47 | var parameter = (NpgsqlRestParameter)parameters[i]; 48 | var typeDescriptor = parameter.TypeDescriptor; 49 | 50 | var suffix = typeDescriptor.IsCastToText() ? 51 | string.Concat(Consts.DoubleColon, typeDescriptor.OriginalType) : 52 | string.Empty; 53 | 54 | if (i > 0) 55 | { 56 | sb.Append(Consts.Comma); 57 | } 58 | 59 | var indexStr = (i + 1).ToString(culture); 60 | 61 | if (typeDescriptor.CustomType is null) 62 | { 63 | if (parameter.ActualName is null) 64 | { 65 | sb.Append(Consts.Dollar) 66 | .Append(indexStr) 67 | .Append(suffix); 68 | } 69 | else 70 | { 71 | sb.Append(parameter.ActualName) 72 | .Append(Consts.NamedParam) 73 | .Append(indexStr); 74 | } 75 | } 76 | else 77 | { 78 | if (typeDescriptor.CustomTypePosition == 1) 79 | { 80 | sb.Append(typeDescriptor.OriginalParameterName) 81 | .Append(Consts.OpenRow); 82 | } 83 | 84 | sb.Append(Consts.Dollar) 85 | .Append(indexStr) 86 | .Append(suffix); 87 | 88 | if (i == count - 1 || 89 | typeDescriptor.CustomTypePosition != 90 | ((NpgsqlRestParameter)parameters[i + 1]).TypeDescriptor.CustomTypePosition - 1) 91 | { 92 | sb.Append(Consts.CloseRow) 93 | .Append(typeDescriptor.CustomType); 94 | } 95 | } 96 | } 97 | 98 | sb.Append(Consts.CloseParenthesis); 99 | return sb.ToString(); 100 | } 101 | } -------------------------------------------------------------------------------- /NpgsqlRest/UploadHandlers/DefaultUploadHandler.cs: -------------------------------------------------------------------------------- 1 | using Npgsql; 2 | 3 | namespace NpgsqlRest.UploadHandlers; 4 | 5 | public class DefaultUploadHandler(params IUploadHandler[] handlers) : IUploadHandler 6 | { 7 | private readonly IUploadHandler[] _handlers = handlers; 8 | private readonly bool _requiresTransaction = handlers.Any(h => h.RequiresTransaction); 9 | 10 | public bool RequiresTransaction => _requiresTransaction; 11 | 12 | public void OnError(NpgsqlConnection? connection, HttpContext context, Exception? exception) 13 | { 14 | for(int i = 0; i < _handlers.Length; i++) 15 | { 16 | _handlers[i].OnError(connection, context, exception); 17 | } 18 | } 19 | 20 | public async Task UploadAsync(NpgsqlConnection connection, HttpContext context, Dictionary? parameters) 21 | { 22 | object[] results = new object[_handlers.Length]; 23 | for (int i = 0; i < _handlers.Length; i++) 24 | { 25 | results[i] = await _handlers[i].UploadAsync(connection, context, parameters); 26 | } 27 | return results; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NpgsqlRest/UploadHandlers/IUploadHandler.cs: -------------------------------------------------------------------------------- 1 | using Npgsql; 2 | 3 | namespace NpgsqlRest.UploadHandlers; 4 | 5 | public interface IUploadHandler 6 | { 7 | /// 8 | /// Set the type of the upload handler. 9 | /// 10 | /// 11 | IUploadHandler SetType(string type) 12 | { 13 | return this; 14 | } 15 | 16 | /// 17 | /// Uploads a file from the context. 18 | /// 19 | /// Opened connection object 20 | /// Http context 21 | /// Upload parameters, specific for each type 22 | /// 23 | /// JSON string with upload metadata that is passed to the upload metadata parameter. 24 | /// It can be array of filename, mime type, size, etc. It depends on implementation. 25 | /// 26 | Task UploadAsync(NpgsqlConnection connection, HttpContext context, Dictionary? parameters); 27 | 28 | /// 29 | /// Connection in Upload call will be under transaction yes or no. 30 | /// 31 | bool RequiresTransaction { get; } 32 | 33 | /// 34 | /// List of parameters that are used in the upload handler. 35 | /// 36 | string[] Parameters => default!; 37 | 38 | /// 39 | /// Runs is the subsequent command fails. 40 | /// 41 | /// Opened connection object 42 | /// 43 | /// 44 | void OnError(NpgsqlConnection? connection, HttpContext context, Exception? exception); 45 | } 46 | -------------------------------------------------------------------------------- /NpgsqlRest/UploadHandlers/UploadHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.UploadHandlers; 2 | 3 | public class UploadHandlerOptions 4 | { 5 | public bool LargeObjectEnabled { get; set; } = true; 6 | public string LargeObjectKey { get; set; } = "large_object"; 7 | public string[]? LargeObjectIncludedMimeTypePatterns { get; set; } = null; 8 | public string[]? LargeObjectExcludedMimeTypePatterns { get; set; } = null; 9 | public int LargeObjectHandlerBufferSize { get; set; } = 8192; 10 | 11 | public bool FileSystemEnabled { get; set; } = true; 12 | public string FileSystemKey { get; set; } = "file_system"; 13 | public string[]? FileSystemIncludedMimeTypePatterns { get; set; } = null; 14 | public string[]? FileSystemExcludedMimeTypePatterns { get; set; } = null; 15 | public string FileSystemHandlerPath { get; set; } = "./"; 16 | public bool FileSystemHandlerUseUniqueFileName { get; set; } = true; 17 | public bool FileSystemHandlerCreatePathIfNotExists { get; set; } = false; 18 | public int FileSystemHandlerBufferSize { get; set; } = 8192; 19 | 20 | public bool CsvUploadEnabled { get; set; } = true; 21 | public string CsvUploadKey { get; set; } = "csv"; 22 | public string[]? CsvUploadIncludedMimeTypePatterns { get; set; } = null; 23 | public string[]? CsvUploadExcludedMimeTypePatterns { get; set; } = null; 24 | public bool CsvUploadCheckFileStatus { get; set; } = true; 25 | public int CsvUploadTestBufferSize { get; set; } = 4096; 26 | public int CsvUploadNonPrintableThreshold { get; set; } = 5; 27 | public string CsvUploadDelimiterChars { get; set; } = ","; 28 | public bool CsvUploadHasFieldsEnclosedInQuotes { get; set; } = true; 29 | public bool CsvUploadSetWhiteSpaceToNull { get; set; } = true; 30 | public string CsvUploadRowCommand { get; set; } = "call process_csv_row($1,$2,$3,$4)"; 31 | } 32 | -------------------------------------------------------------------------------- /NpgsqlRest/UploadHandlers/UploadLog.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.UploadHandlers; 2 | 3 | public static partial class UploadLog 4 | { 5 | [LoggerMessage(Level = LogLevel.Warning, Message = "File {fileName} ({contentType}, {length} bytes) is not a valid CSV file. Status: {status}")] 6 | public static partial void NotValidCsvFile(this ILogger logger, string fileName, string contentType, long length, string status); 7 | 8 | [LoggerMessage(Level = LogLevel.Information, Message = "Uploaded file {fileName} {contentType}, {length} bytes) as CSV using command {command}")] 9 | public static partial void UploadedCsvFile(this ILogger logger, string fileName, string contentType, long length, string command); 10 | 11 | [LoggerMessage(Level = LogLevel.Information, Message = "Uploaded file {fileName} {contentType}, {length} bytes) to file path {currentFilePath}")] 12 | public static partial void UploadedFileToFileSystem(this ILogger logger, string fileName, string contentType, long length, string currentFilePath); 13 | 14 | [LoggerMessage(Level = LogLevel.Information, Message = "Uploaded file {fileName} {contentType}, {length} bytes) to large object {resultOid}")] 15 | public static partial void UploadedFileToLargeObject(this ILogger logger, string fileName, string contentType, long length, object? resultOid); 16 | } -------------------------------------------------------------------------------- /NpgsqlRestClient/DbDataProtection.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using Microsoft.AspNetCore.DataProtection.Repositories; 3 | using Npgsql; 4 | 5 | namespace NpgsqlRestClient; 6 | 7 | public class DbDataProtection(string? connectionString, string getCommand, string storeCommand) 8 | : IXmlRepository 9 | { 10 | private readonly string? _connectionString = connectionString; 11 | private readonly string _getCommand = getCommand; 12 | private readonly string _storeCommand = storeCommand; 13 | 14 | public IReadOnlyCollection GetAllElements() 15 | { 16 | var elements = new List(); 17 | 18 | using (var connection = new NpgsqlConnection(_connectionString)) 19 | { 20 | connection.Open(); 21 | using var cmd = new NpgsqlCommand(_getCommand, connection); 22 | using var reader = cmd.ExecuteReader(); 23 | while (reader.Read()) 24 | { 25 | elements.Add(XElement.Parse(reader.GetString(0))); 26 | } 27 | } 28 | 29 | return elements; 30 | } 31 | 32 | public void StoreElement(XElement element, string friendlyName) 33 | { 34 | using var connection = new NpgsqlConnection(_connectionString); 35 | connection.Open(); 36 | using var cmd = new NpgsqlCommand(); 37 | cmd.Connection = connection; 38 | cmd.CommandText = _storeCommand; 39 | cmd.Parameters.Add(new NpgsqlParameter() { Value = friendlyName }); // $1 40 | cmd.Parameters.Add(new NpgsqlParameter() { Value = element.ToString(SaveOptions.DisableFormatting) }); // $2 41 | cmd.ExecuteNonQuery(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NpgsqlRestClient/DbLogging.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Npgsql; 3 | using Serilog; 4 | using Serilog.Configuration; 5 | using Serilog.Core; 6 | using Serilog.Events; 7 | 8 | namespace NpgsqlRestClient; 9 | 10 | public class PostgresSink(string command, LogEventLevel restrictedToMinimumLevel, int paramCount) : ILogEventSink 11 | { 12 | private readonly string _command = command; 13 | private readonly LogEventLevel _restrictedToMinimumLevel = restrictedToMinimumLevel; 14 | private readonly int _paramCount = paramCount; 15 | 16 | public void Emit(LogEvent logEvent) 17 | { 18 | if (string.IsNullOrEmpty(Builder.ConnectionString) is true) 19 | { 20 | return; 21 | } 22 | if (logEvent.Level < _restrictedToMinimumLevel) 23 | { 24 | return; 25 | } 26 | 27 | try 28 | { 29 | using var connection = new NpgsqlConnection(Builder.ConnectionString); 30 | using var command = new NpgsqlCommand(_command, connection); 31 | 32 | if (_paramCount > 0) 33 | { 34 | command.Parameters.Add(new NpgsqlParameter() { Value = logEvent.Level.ToString() }); // $1 35 | } 36 | if (_paramCount > 1) 37 | { 38 | command.Parameters.Add(new NpgsqlParameter() { Value = logEvent.RenderMessage() }); // $2 39 | } 40 | if (_paramCount > 2) 41 | { 42 | command.Parameters.Add(new NpgsqlParameter() { Value = logEvent.Timestamp.UtcDateTime }); // $3 43 | } 44 | if (_paramCount > 3) 45 | { 46 | command.Parameters.Add(new NpgsqlParameter() { Value = logEvent.Exception?.ToString() ?? (object)DBNull.Value }); // $4 47 | } 48 | if (_paramCount > 4) 49 | { 50 | command.Parameters.Add(new NpgsqlParameter() { Value = logEvent.Properties["SourceContext"]?.ToString()?.Trim('"') ?? (object)DBNull.Value }); // $5 51 | } 52 | connection.Open(); 53 | command.ExecuteNonQuery(); 54 | } 55 | catch (Exception ex) 56 | { 57 | Console.ForegroundColor = ConsoleColor.Red; 58 | Console.WriteLine("Error writing to Postgres Log Sink:"); 59 | Console.WriteLine(ex); 60 | Console.ResetColor(); 61 | } 62 | } 63 | } 64 | 65 | public static partial class PostgresSinkSinkExtensions 66 | { 67 | public static LoggerConfiguration Postgres(this LoggerSinkConfiguration loggerConfiguration, 68 | string command, 69 | LogEventLevel restrictedToMinimumLevel) 70 | { 71 | var matches = ParameterRegex().Matches(command).ToArray(); 72 | if (matches.Length < 1 || matches.Length > 5) 73 | { 74 | throw new ArgumentException("Command should have at least one parameter and maximum five parameters."); 75 | } 76 | for(int i = 0; i < matches.Length; i++) 77 | { 78 | if (matches[i].Value != $"${i + 1}") 79 | { 80 | throw new ArgumentException($"Parameter ${i + 1} is missing in the command."); 81 | } 82 | } 83 | return loggerConfiguration.Sink(new PostgresSink(command, restrictedToMinimumLevel, matches.Length)); 84 | } 85 | 86 | [GeneratedRegex(@"\$\d+")] 87 | public static partial Regex ParameterRegex(); 88 | } 89 | -------------------------------------------------------------------------------- /NpgsqlRestClient/DefaultParser.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Antiforgery; 3 | using Microsoft.Extensions.Primitives; 4 | using NpgsqlRest; 5 | 6 | namespace NpgsqlRestClient; 7 | 8 | public class DefaultResponseParser( 9 | string? userIdParameterName, 10 | string? userNameParameterName, 11 | string? userRolesParameterName, 12 | string? ipAddressParameterName, 13 | string? antiforgeryFieldNameTag, 14 | string? antiforgeryTokenTag, 15 | Dictionary? customClaims, 16 | Dictionary? customParameters) : IResponseParser 17 | { 18 | private readonly string? userIdParameterName = userIdParameterName; 19 | private readonly string? userNameParameterName = userNameParameterName; 20 | private readonly string? userRolesParameterName = userRolesParameterName; 21 | private readonly string? ipAddressParameterName = ipAddressParameterName; 22 | 23 | private readonly string? antiforgeryFieldNameTag = antiforgeryFieldNameTag; 24 | private readonly string? antiforgeryTokenTag = antiforgeryTokenTag; 25 | 26 | private readonly Dictionary? customClaims = customClaims; 27 | private readonly Dictionary? customParameters = customParameters; 28 | 29 | public ReadOnlySpan Parse(ReadOnlySpan input, RoutineEndpoint endpoint, HttpContext context) 30 | { 31 | return Parse(input, context, null); 32 | } 33 | 34 | public ReadOnlySpan Parse(ReadOnlySpan input, HttpContext context, AntiforgeryTokenSet? tokenSet) 35 | { 36 | Dictionary replacements = []; 37 | 38 | if (userIdParameterName is not null) 39 | { 40 | var value = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; 41 | replacements.Add(userIdParameterName, value is null ? Consts.Null : string.Concat(Consts.DoubleQuote, value, Consts.DoubleQuote)); 42 | } 43 | if (userNameParameterName is not null) 44 | { 45 | var value = context.User.Identity?.Name; 46 | replacements.Add(userNameParameterName, value is null ? Consts.Null : string.Concat(Consts.DoubleQuote, value, Consts.DoubleQuote)); 47 | } 48 | if (userRolesParameterName is not null) 49 | { 50 | var value = context.User.FindAll(c => string.Equals(c.Type, ClaimTypes.Role, StringComparison.Ordinal))?.Select(r => string.Concat(Consts.DoubleQuote, r.Value, Consts.DoubleQuote)); 51 | replacements.Add(userRolesParameterName, value is null ? Consts.Null : string.Concat(Consts.OpenBracket, string.Join(Consts.Comma, value), Consts.CloseBracket)); 52 | } 53 | if (ipAddressParameterName is not null) 54 | { 55 | var value = context.Request.GetClientIpAddress(); 56 | replacements.Add(ipAddressParameterName, value is null ? Consts.Null : string.Concat(Consts.DoubleQuote, value, Consts.DoubleQuote)); 57 | } 58 | if (customClaims is not null) 59 | { 60 | foreach (var (key, value) in customClaims) 61 | { 62 | var claim = context.User.FindFirst(key); 63 | replacements.Add(key, claim is null ? Consts.Null : string.Concat(Consts.DoubleQuote, claim.Value, Consts.DoubleQuote)); 64 | } 65 | } 66 | if (customParameters is not null) 67 | { 68 | foreach (var (key, value) in customParameters) 69 | { 70 | replacements.Add(key, value is null ? Consts.Null : string.Concat(Consts.DoubleQuote, value, Consts.DoubleQuote)); 71 | } 72 | } 73 | if (tokenSet is not null && (antiforgeryFieldNameTag is not null || antiforgeryTokenTag is not null)) 74 | { 75 | if (antiforgeryFieldNameTag is not null) 76 | { 77 | replacements.Add(antiforgeryFieldNameTag, tokenSet.FormFieldName); 78 | } 79 | if (antiforgeryTokenTag is not null && tokenSet.RequestToken is not null) 80 | { 81 | replacements.Add(antiforgeryTokenTag, tokenSet.RequestToken); 82 | } 83 | } 84 | return Formatter.FormatString(input, replacements); 85 | } 86 | } -------------------------------------------------------------------------------- /NpgsqlRestClient/NpgsqlRestClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 13.0 6 | enable 7 | enable 8 | true 9 | true 10 | true 11 | 2.22.0 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /NpgsqlRestClient/TokenRefreshAuth.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.Json.Nodes; 3 | using Microsoft.AspNetCore.Http.HttpResults; 4 | using Microsoft.Extensions.Options; 5 | using NpgsqlRest; 6 | 7 | namespace NpgsqlRestClient; 8 | 9 | public class BearerTokenConfig 10 | { 11 | public string? Scheme { get; set; } 12 | public string? RefreshPath { get; set; } 13 | } 14 | 15 | public static class TokenRefreshAuth 16 | { 17 | public static void Configure(WebApplication app) 18 | { 19 | if (Builder.BearerTokenConfig is null || 20 | string.IsNullOrEmpty(Builder.BearerTokenConfig.RefreshPath) is true || 21 | string.IsNullOrEmpty(Builder.BearerTokenConfig.Scheme) is true) 22 | { 23 | return; 24 | } 25 | 26 | app.Use(async (context, next) => 27 | { 28 | if (context.Request.Path.Equals(Builder.BearerTokenConfig.RefreshPath, StringComparison.OrdinalIgnoreCase) is false) 29 | { 30 | await next(context); 31 | return; 32 | } 33 | 34 | if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) is false) 35 | { 36 | await next(context); 37 | return; 38 | } 39 | 40 | var bearerTokenOptions = app.Services.GetRequiredService>(); 41 | var refreshTokenProtector = bearerTokenOptions.Get(Builder.BearerTokenConfig.Scheme).RefreshTokenProtector; 42 | var timeProvider = app.Services.GetRequiredService(); 43 | 44 | string refreshToken; 45 | IResult result; 46 | 47 | try 48 | { 49 | using var reader = new StreamReader(context.Request.Body); 50 | var body = await reader.ReadToEndAsync(); 51 | var node = JsonNode.Parse(string.IsNullOrWhiteSpace(body) ? "{}" : body); 52 | refreshToken = node!["refresh"]?.ToString() ?? throw new ArgumentException("refresh token is null"); 53 | } 54 | catch (Exception ex) 55 | { 56 | NpgsqlRestMiddleware.Logger?.LogError(ex, "Failed to read refresh token from request body."); 57 | result = Results.BadRequest(context.Response); 58 | await result.ExecuteAsync(context); 59 | return; 60 | } 61 | 62 | var refreshTicket = refreshTokenProtector.Unprotect(refreshToken); 63 | if ( 64 | (refreshTicket?.Properties?.ExpiresUtc is not { } expiresUtc || timeProvider.GetUtcNow() >= expiresUtc) || 65 | context.User.Identity?.IsAuthenticated is false) 66 | { 67 | result = Results.Challenge(); 68 | await result.ExecuteAsync(context); 69 | return; 70 | } 71 | 72 | if (Results.SignIn(principal: context.User, authenticationScheme: Builder.BearerTokenConfig.Scheme) is not SignInHttpResult signInResult) 73 | { 74 | NpgsqlRestMiddleware.Logger?.LogError("Failed in constructing user identity for authentication."); 75 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 76 | return; 77 | } 78 | await signInResult.ExecuteAsync(context); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /NpgsqlRestTests/AuthTests/AuthorizedTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void AuthTests() 6 | { 7 | script.Append(""" 8 | create function authorized() returns text language sql as 'select ''authorized'''; 9 | comment on function authorized() is 'authorize'; 10 | 11 | create function authorized_roles1() returns text language sql as 'select ''roles1'''; 12 | comment on function authorized_roles1() is 'authorize test_role'; 13 | 14 | create function authorized_roles2() returns text language sql as 'select ''roles2'''; 15 | comment on function authorized_roles2() is 'authorize test_role, role1'; 16 | 17 | create function authorized_roles3() returns text language sql as 'select ''roles3'''; 18 | comment on function authorized_roles3() is 'authorize test_role1 role1 test_role2 test_role1'; 19 | 20 | create function authorized_roles4() returns text language sql as 'select ''roles4'''; 21 | comment on function authorized_roles4() is 'authorize test_role1 test_role2 test_role3'; 22 | """); 23 | } 24 | } 25 | 26 | [Collection("TestFixture")] 27 | public class AuthorizedTests(TestFixture test) 28 | { 29 | [Fact] 30 | public async Task Test_authorized() 31 | { 32 | using var client = test.Application.CreateClient(); 33 | client.Timeout = TimeSpan.FromHours(1); 34 | 35 | using var response1 = await client.PostAsync("/api/authorized/", null); 36 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 37 | 38 | using var login = await client.GetAsync("/login"); 39 | 40 | using var response2 = await client.PostAsync("/api/authorized/", null); 41 | response2.StatusCode.Should().Be(HttpStatusCode.OK); 42 | } 43 | 44 | [Fact] 45 | public async Task Test_authorized_roles1() 46 | { 47 | using var client = test.Application.CreateClient(); 48 | client.Timeout = TimeSpan.FromHours(1); 49 | 50 | using var response1 = await client.PostAsync("/api/authorized-roles1/", null); 51 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 52 | 53 | using var login = await client.GetAsync("/login"); 54 | 55 | using var response2 = await client.PostAsync("/api/authorized-roles1/", null); 56 | response2.StatusCode.Should().Be(HttpStatusCode.Forbidden); 57 | } 58 | 59 | [Fact] 60 | public async Task Test_authorized_roles2() 61 | { 62 | using var client = test.Application.CreateClient(); 63 | client.Timeout = TimeSpan.FromHours(1); 64 | 65 | using var response1 = await client.PostAsync("/api/authorized-roles2/", null); 66 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 67 | 68 | using var login = await client.GetAsync("/login"); 69 | 70 | using var response2 = await client.PostAsync("/api/authorized-roles2/", null); 71 | response2.StatusCode.Should().Be(HttpStatusCode.OK); 72 | } 73 | 74 | [Fact] 75 | public async Task Test_authorized_roles3() 76 | { 77 | using var client = test.Application.CreateClient(); 78 | client.Timeout = TimeSpan.FromHours(1); 79 | 80 | using var response1 = await client.PostAsync("/api/authorized-roles3/", null); 81 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 82 | 83 | using var login = await client.GetAsync("/login"); 84 | 85 | using var response2 = await client.PostAsync("/api/authorized-roles3/", null); 86 | response2.StatusCode.Should().Be(HttpStatusCode.OK); 87 | } 88 | 89 | [Fact] 90 | public async Task Test_authorized_roles4() 91 | { 92 | using var client = test.Application.CreateClient(); 93 | client.Timeout = TimeSpan.FromHours(1); 94 | 95 | using var response1 = await client.PostAsync("/api/authorized-roles4/", null); 96 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 97 | 98 | using var login = await client.GetAsync("/login"); 99 | 100 | using var response2 = await client.PostAsync("/api/authorized-roles4/", null); 101 | response2.StatusCode.Should().Be(HttpStatusCode.Forbidden); 102 | } 103 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/AuthTests/PasswordHasherTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests.AuthTests; 2 | 3 | public class PasswordHasherTests 4 | { 5 | private readonly PasswordHasher _hasher; 6 | 7 | public PasswordHasherTests() 8 | { 9 | _hasher = new PasswordHasher(); 10 | } 11 | 12 | [Fact] 13 | public void HashPassword_ValidPassword_ReturnsBase64Hash() 14 | { 15 | // Arrange 16 | string password = "MySecurePassword123!"; 17 | 18 | // Act 19 | string hashedPassword = _hasher.HashPassword(password); 20 | 21 | // Assert 22 | hashedPassword.Should().NotBeNullOrEmpty(); 23 | hashedPassword.Should().NotBe(password); // Ensure it's not plaintext 24 | byte[] decoded = Convert.FromBase64String(hashedPassword); 25 | decoded.Should().HaveCount(16 + 32); // 16-byte salt + 32-byte hash 26 | } 27 | 28 | [Fact] 29 | public void HashPassword_NullPassword_ThrowsArgumentException() 30 | { 31 | // Arrange 32 | string password = null!; 33 | 34 | // Act & Assert 35 | Action act = () => _hasher.HashPassword(password); 36 | act.Should().Throw() 37 | .WithMessage("Password cannot be null or empty. (Parameter 'password')"); 38 | } 39 | 40 | [Fact] 41 | public void HashPassword_EmptyPassword_ThrowsArgumentException() 42 | { 43 | // Arrange 44 | string password = ""; 45 | 46 | // Act & Assert 47 | Action act = () => _hasher.HashPassword(password); 48 | act.Should().Throw() 49 | .WithMessage("Password cannot be null or empty. (Parameter 'password')"); 50 | } 51 | 52 | [Fact] 53 | public void VerifyHashedPassword_CorrectPassword_ReturnsTrue() 54 | { 55 | // Arrange 56 | string password = "MySecurePassword123!"; 57 | string hashedPassword = _hasher.HashPassword(password); 58 | 59 | // Act 60 | bool isValid = _hasher.VerifyHashedPassword(hashedPassword, password); 61 | 62 | // Assert 63 | isValid.Should().BeTrue(); 64 | } 65 | 66 | [Fact] 67 | public void VerifyHashedPassword_NullOrEmptyInputs_ReturnsFalse() 68 | { 69 | // Arrange 70 | string password = "MySecurePassword123!"; 71 | string hashedPassword = _hasher.HashPassword(password); 72 | 73 | // Act & Assert 74 | _hasher.VerifyHashedPassword(null!, password).Should().BeFalse(); 75 | _hasher.VerifyHashedPassword(hashedPassword, null!).Should().BeFalse(); 76 | _hasher.VerifyHashedPassword("", password).Should().BeFalse(); 77 | _hasher.VerifyHashedPassword(hashedPassword, "").Should().BeFalse(); 78 | } 79 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/BodyTests/BodyParameterNameTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void BodyParameterNameTests() 6 | { 7 | script.Append(@" 8 | create function body_param_name_1p(_p text) 9 | returns text language sql as 'select _p'; 10 | comment on function body_param_name_1p(text) is ' 11 | HTTP 12 | body_param_name p 13 | '; 14 | 15 | create function body_param_name_2p(_i int, _p text) 16 | returns text language sql as 'select _i::text || '' '' || _p'; 17 | comment on function body_param_name_2p(int, text) is ' 18 | HTTP 19 | body_param_name _p 20 | '; 21 | 22 | create function body_param_name_int(_int int) 23 | returns text language sql as 'select _int'; 24 | comment on function body_param_name_int(int) is ' 25 | HTTP 26 | body_param_name int'; 27 | "); 28 | } 29 | } 30 | 31 | [Collection("TestFixture")] 32 | public class BodyParameterNameTests(TestFixture test) 33 | { 34 | [Fact] 35 | public async Task Test_body_param_name_1p() 36 | { 37 | using var body = new StringContent("test 123", Encoding.UTF8); 38 | using var response = await test.Client.PostAsync("/api/body-param-name-1p/", body); 39 | var content = await response.Content.ReadAsStringAsync(); 40 | 41 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 42 | content.Should().Be("test 123"); 43 | } 44 | 45 | [Fact] 46 | public async Task Test_body_param_name_2p() 47 | { 48 | using var body = new StringContent("test ABC", Encoding.UTF8); 49 | using var response = await test.Client.PostAsync("/api/body-param-name-2p/?i=123", body); 50 | var content = await response.Content.ReadAsStringAsync(); 51 | 52 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 53 | content.Should().Be("123 test ABC"); 54 | } 55 | 56 | [Fact] 57 | public async Task Test_body_param_name_int() 58 | { 59 | using var body = new StringContent("123", Encoding.UTF8); 60 | using var response = await test.Client.PostAsync("/api/body-param-name-int", body); 61 | var content = await response.Content.ReadAsStringAsync(); 62 | 63 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 64 | content.Should().Be("123"); 65 | } 66 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/BodyTests/BodyParamsTests.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 2 | namespace NpgsqlRestTests; 3 | 4 | public static partial class Database 5 | { 6 | public static void BodyParamsTests() 7 | { 8 | script.Append(@" 9 | create function body_params_test1( 10 | _i int, 11 | _t text 12 | ) 13 | returns text 14 | language sql as $$select 1 || '-' || _t;$$; 15 | "); 16 | } 17 | } 18 | 19 | [Collection("TestFixture")] 20 | public class BodyParamsTests(TestFixture test) 21 | { 22 | [Fact] 23 | public async Task Test_body_params_test1() 24 | { 25 | string body = """ 26 | { 27 | "i": 666, 28 | "t": "numberofthebeast" 29 | } 30 | """; 31 | using var content = new StringContent(body, Encoding.UTF8, "application/json"); 32 | using var response = await test.Client.PostAsync("/api/body-params-test1/", content); 33 | response.StatusCode.Should().Be(HttpStatusCode.OK); 34 | response.Content.Headers.ContentType.MediaType.Should().Be("text/plain"); 35 | var result = await response.Content.ReadAsStringAsync(); 36 | result.Should().Be("1-numberofthebeast"); 37 | } 38 | 39 | [Fact] 40 | public async Task Test_body_params_test1_reverse() 41 | { 42 | string body = """ 43 | { 44 | "t": "numberofthebeast", 45 | "i": 666 46 | } 47 | """; 48 | using var content = new StringContent(body, Encoding.UTF8, "application/json"); 49 | using var response = await test.Client.PostAsync("/api/body-params-test1/", content); 50 | response.StatusCode.Should().Be(HttpStatusCode.OK); 51 | response.Content.Headers.ContentType.MediaType.Should().Be("text/plain"); 52 | var result = await response.Content.ReadAsStringAsync(); 53 | result.Should().Be("1-numberofthebeast"); 54 | } 55 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CommandCallbackTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CommandCallbackTests() 6 | { 7 | script.Append(@" 8 | create function get_csv_data() 9 | returns table (id int, name text, date timestamp, status boolean) 10 | language sql as 11 | $$ 12 | select * from ( 13 | values 14 | (1, 'foo', '2024-01-31'::timestamp, true), 15 | (2, 'bar', '2024-01-29'::timestamp, true), 16 | (3, 'xyz', '2024-01-25'::timestamp, false) 17 | ) t; 18 | $$; 19 | "); 20 | } 21 | } 22 | 23 | [Collection("TestFixture")] 24 | public class CommandCallbackTests(TestFixture test) 25 | { 26 | [Fact] 27 | public async Task Test_get_csv_data() 28 | { 29 | using var response = await test.Client.GetAsync("/api/get-csv-data/"); 30 | var content = await response.Content.ReadAsStringAsync(); 31 | 32 | response.StatusCode.Should().Be(HttpStatusCode.OK); 33 | response.Content.Headers.ContentType?.MediaType.Should().Be("text/csv"); 34 | 35 | content.Should().Be(string.Join('\n', [ 36 | "1,foo,2024-01-31T00:00:00,true", 37 | "2,bar,2024-01-29T00:00:00,true", 38 | "3,xyz,2024-01-25T00:00:00,false", 39 | "" 40 | ])); 41 | } 42 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CommentTests/CommentAuthAttrTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CommentAuthAttrTests() 6 | { 7 | script.Append(@" 8 | create function comment_authorize1() returns text language sql as 'select ''authorize1'''; 9 | comment on function comment_authorize1() is 'HTTP 10 | Authorize'; 11 | 12 | create function comment_authorize2() returns text language sql as 'select ''authorize2'''; 13 | comment on function comment_authorize2() is 'HTTP 14 | requires_authorization'; 15 | 16 | create function comment_authorize3() returns text language sql as 'select ''authorize3'''; 17 | comment on function comment_authorize3() is ' 18 | Authorize'; 19 | 20 | create function comment_authorize4() returns text language sql as 'select ''authorize4'''; 21 | comment on function comment_authorize4() is 'Authorize 22 | HTTP'; 23 | "); 24 | } 25 | } 26 | 27 | [Collection("TestFixture")] 28 | public class CommentAuthAttrTests(TestFixture test) 29 | { 30 | [Fact] 31 | public async Task Test_comment_authorize1() 32 | { 33 | using var response1 = await test.Client.PostAsync("/api/comment-authorize1/", null); 34 | response1?.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task Test_comment_authorize2() 39 | { 40 | using var response1 = await test.Client.PostAsync("/api/comment-authorize2/", null); 41 | response1?.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 42 | } 43 | 44 | [Fact] 45 | public async Task Test_comment_authorize3() 46 | { 47 | using var response1 = await test.Client.PostAsync("/api/comment-authorize3/", null); 48 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 49 | } 50 | 51 | [Fact] 52 | public async Task Test_comment_authorize4() 53 | { 54 | using var response1 = await test.Client.PostAsync("/api/comment-authorize4/", null); 55 | response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized); 56 | } 57 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CommentTests/CommentHttpAttrTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CommentHttpAttrTests() 6 | { 7 | script.Append(@" 8 | create function comment_verb_test1() returns text language sql as 'select ''verb test1'''; 9 | comment on function comment_verb_test1() is 'HTTP GET'; 10 | 11 | create function comment_verb_test2() returns text language sql as 'select ''verb test2'''; 12 | comment on function comment_verb_test2() is 'This is some comment. 13 | Some other comment. 14 | HTTP GET 15 | And again some other comment.'; 16 | 17 | create function comment_verb_test3() returns text language sql as 'select ''verb test3'''; 18 | comment on function comment_verb_test3() is 'HTTP GET custom_url_from_comment'; 19 | 20 | create function comment_wrong_verb() returns text language sql as 'select ''wrong verb'''; 21 | comment on function comment_wrong_verb() is 'HTTP wrong-verb'; 22 | 23 | create function comment_http_path() returns text language sql as 'select ''new path'''; 24 | comment on function comment_http_path() is 'HTTP GET 25 | PATH new-path'; 26 | "); 27 | } 28 | } 29 | 30 | [Collection("TestFixture")] 31 | public class CommentHttpAttrTests(TestFixture test) 32 | { 33 | [Fact] 34 | public async Task Test_comment_verb_test1() 35 | { 36 | using var response = await test.Client.GetAsync("/api/comment-verb-test1/"); 37 | await AssertResponse(response, "verb test1"); 38 | } 39 | 40 | [Fact] 41 | public async Task Test_comment_verb_test2() 42 | { 43 | using var response = await test.Client.GetAsync("/api/comment-verb-test2/"); 44 | await AssertResponse(response, "verb test2"); 45 | } 46 | 47 | [Fact] 48 | public async Task Test_comment_verb_test3() 49 | { 50 | using var response = await test.Client.GetAsync("/custom_url_from_comment"); 51 | await AssertResponse(response, "verb test3"); 52 | } 53 | 54 | [Fact] 55 | public async Task Test_comment_wrong_verb() 56 | { 57 | using var response1 = await test.Client.GetAsync("/wrong-verb"); 58 | response1?.StatusCode.Should().Be(HttpStatusCode.NotFound); 59 | 60 | using var response2 = await test.Client.PostAsync("/wrong-verb", null); 61 | //response2?.StatusCode.Should().Be(HttpStatusCode.NotFound); 62 | await AssertResponse(response2, "wrong verb"); 63 | 64 | using var response3 = await test.Client.PostAsync("/api/comment-wrong-verb/", null); 65 | //await AssertResponse(response3, "wrong verb"); 66 | response3?.StatusCode.Should().Be(HttpStatusCode.NotFound); 67 | } 68 | 69 | [Fact] 70 | public async Task Test_comment_new_path() 71 | { 72 | using var response1 = await test.Client.GetAsync("/new-path"); 73 | await AssertResponse(response1, "new path"); 74 | } 75 | 76 | private static async Task AssertResponse(HttpResponseMessage response, string expectedContent) 77 | { 78 | var content = await response.Content.ReadAsStringAsync(); 79 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 80 | response?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 81 | content.Should().Be(expectedContent); 82 | } 83 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CommentTests/CommentResponseHeadersTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CommentResponseHeadersTests() 6 | { 7 | script.Append(@" 8 | create function hello_world_html() returns text language sql as 'select ''
Hello World
'''; 9 | comment on function hello_world_html() is ' 10 | HTTP GET 11 | Content-Type: text/html'; 12 | 13 | create function comment_response_headers() returns text language sql as 'select ''comment_response_headers'''; 14 | comment on function comment_response_headers() is ' 15 | HTTP 16 | Content-Type: application/json 17 | CustomHeader: test 18 | MultiValueCustomHeader: test1 19 | MultiValueCustomHeader: test2 20 | cache-control: no-store 21 | '; 22 | "); 23 | } 24 | } 25 | 26 | [Collection("TestFixture")] 27 | public class CommentResponseHeadersTests(TestFixture test) 28 | { 29 | [Fact] 30 | public async Task Test_hello_world_html() 31 | { 32 | using var result = await test.Client.GetAsync("/api/hello-world-html/"); 33 | var response = await result.Content.ReadAsStringAsync(); 34 | 35 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 36 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/html"); 37 | response.Should().Be("
Hello World
"); 38 | } 39 | 40 | [Fact] 41 | public async Task Test_comment_response_headers() 42 | { 43 | using var response = await test.Client.PostAsync("/api/comment-response-headers/", null); 44 | var content = await response.Content.ReadAsStringAsync(); 45 | 46 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 47 | content.Should().Be("comment_response_headers"); 48 | 49 | response?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 50 | 51 | // Note: test client does not support custom headers check out headers in the http client, following headers should be present: 52 | // Cache-Control no-store 53 | // CustomHeader test 54 | // MultiValueCustomHeader test1, test2 55 | } 56 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ConnectionNameTests/ConnectionNameTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ConnectionNameTests() 6 | { 7 | script.Append(@" 8 | create function get_default_connection_name() 9 | returns text 10 | language sql 11 | as $$ 12 | select application_name 13 | from pg_stat_activity 14 | where pid = pg_backend_pid(); 15 | $$; 16 | 17 | create function get_conn1_connection_name() 18 | returns text 19 | language sql 20 | as $$ 21 | select application_name 22 | from pg_stat_activity 23 | where pid = pg_backend_pid(); 24 | $$; 25 | 26 | comment on function get_conn1_connection_name() is 'connection_name conn1'; 27 | 28 | create function get_conn2_connection_name() 29 | returns text 30 | language sql 31 | as $$ 32 | select application_name 33 | from pg_stat_activity 34 | where pid = pg_backend_pid(); 35 | $$; 36 | 37 | comment on function get_conn2_connection_name() is 'connection_name conn2'; 38 | 39 | create function get_conn3_connection_name() 40 | returns text 41 | language sql 42 | as $$ 43 | select application_name 44 | from pg_stat_activity 45 | where pid = pg_backend_pid(); 46 | $$; 47 | 48 | comment on function get_conn3_connection_name() is 'connection_name conn3'; 49 | "); 50 | } 51 | } 52 | 53 | [Collection("TestFixture")] 54 | public class ConnectionNameTests(TestFixture test) 55 | { 56 | [Fact] 57 | public async Task Test_get_default_connection_name() 58 | { 59 | using var response = await test.Client.GetAsync("/api/get-default-connection-name/"); 60 | var content = await response.Content.ReadAsStringAsync(); 61 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 62 | content.Should().Be(""); 63 | } 64 | 65 | [Fact] 66 | public async Task Test_get_conn1_connection_name() 67 | { 68 | using var response = await test.Client.GetAsync("/api/get-conn1-connection-name/"); 69 | var content = await response.Content.ReadAsStringAsync(); 70 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 71 | content.Should().Be("conn1"); 72 | } 73 | 74 | [Fact] 75 | public async Task Test_get_conn2_connection_name() 76 | { 77 | using var response = await test.Client.GetAsync("/api/get-conn2-connection-name/"); 78 | var content = await response.Content.ReadAsStringAsync(); 79 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 80 | content.Should().Be("conn2"); 81 | } 82 | 83 | [Fact] 84 | public async Task Test_get_conn3_connection_name() 85 | { 86 | using var response = await test.Client.GetAsync("/api/get-conn3-connection-name/"); 87 | var content = await response.Content.ReadAsStringAsync(); 88 | response?.StatusCode.Should().Be(HttpStatusCode.InternalServerError); 89 | content.Should().Be("Connection name conn3 could not be found in options ConnectionStrings dictionary."); 90 | } 91 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ConnectionNameTests/ConnectionNameUsingParamsTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ConnectionNameUsingParamsTests() 6 | { 7 | script.Append(@" 8 | create function get_conn1_connection_name_p() 9 | returns text 10 | language sql 11 | as $$ 12 | select application_name 13 | from pg_stat_activity 14 | where pid = pg_backend_pid(); 15 | $$; 16 | 17 | comment on function get_conn1_connection_name_p() is 'connection_name=conn1'; 18 | 19 | create function get_conn2_connection_name_p() 20 | returns text 21 | language sql 22 | as $$ 23 | select application_name 24 | from pg_stat_activity 25 | where pid = pg_backend_pid(); 26 | $$; 27 | 28 | comment on function get_conn2_connection_name_p() is 'connection=conn2'; 29 | 30 | create function get_conn3_connection_name_p() 31 | returns text 32 | language sql 33 | as $$ 34 | select application_name 35 | from pg_stat_activity 36 | where pid = pg_backend_pid(); 37 | $$; 38 | 39 | comment on function get_conn3_connection_name_p() is 'connection_name=conn3'; 40 | "); 41 | } 42 | } 43 | 44 | [Collection("TestFixture")] 45 | public class ConnectionNameUsingParamsTests(TestFixture test) 46 | { 47 | [Fact] 48 | public async Task Test_get_conn1_connection_name_p() 49 | { 50 | using var response = await test.Client.GetAsync("/api/get-conn1-connection-name-p/"); 51 | var content = await response.Content.ReadAsStringAsync(); 52 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 53 | content.Should().Be("conn1"); 54 | } 55 | 56 | [Fact] 57 | public async Task Test_get_conn2_connection_name_p() 58 | { 59 | using var response = await test.Client.GetAsync("/api/get-conn2-connection-name-p/"); 60 | var content = await response.Content.ReadAsStringAsync(); 61 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 62 | content.Should().Be("conn2"); 63 | } 64 | 65 | [Fact] 66 | public async Task Test_get_conn3_connection_name_p() 67 | { 68 | using var response = await test.Client.GetAsync("/api/get-conn3-connection-name-p/"); 69 | var content = await response.Content.ReadAsStringAsync(); 70 | response?.StatusCode.Should().Be(HttpStatusCode.InternalServerError); 71 | content.Should().Be("Connection name conn3 could not be found in options ConnectionStrings dictionary."); 72 | } 73 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CrudTests/CrudDeleteTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CrudDeleteTests() 6 | { 7 | script.Append(""" 8 | create table crud_delete1 ( 9 | id int, 10 | name text 11 | ); 12 | insert into crud_delete1 values (1,'name1'),(2,'name2'),(3,'name3'),(4,'name4'); 13 | 14 | create table crud_delete2 ( 15 | id int, 16 | name text 17 | ); 18 | insert into crud_delete2 values (1,'name1'),(2,'name2'),(3,'name3'),(4,'name4'); 19 | """); 20 | } 21 | } 22 | 23 | [Collection("TestFixture")] 24 | public class CrudDeleteTests(TestFixture test) 25 | { 26 | [Fact] 27 | public async Task Test_crud_delete1() 28 | { 29 | using var response = await test.Client.DeleteAsync("/api/crud-delete1/?id=1"); 30 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 31 | 32 | using var getResponse = await test.Client.GetAsync("/api/crud-delete1/"); 33 | var content = await getResponse.Content.ReadAsStringAsync(); 34 | 35 | content.Should().Be(""" 36 | [ 37 | {"id":2,"name":"name2"}, 38 | {"id":3,"name":"name3"}, 39 | {"id":4,"name":"name4"} 40 | ] 41 | """ 42 | .Replace(" ", "") 43 | .Replace("\n", "") 44 | .Replace("\r", "") 45 | .Trim()); 46 | 47 | using var response2 = await test.Client.DeleteAsync("/api/crud-delete1/"); 48 | response2.StatusCode.Should().Be(HttpStatusCode.NoContent); 49 | 50 | using var getResponse2 = await test.Client.GetAsync("/api/crud-delete1/"); 51 | var content2 = await getResponse2.Content.ReadAsStringAsync(); 52 | 53 | content2.Should().Be("[]"); 54 | } 55 | 56 | [Fact] 57 | public async Task Test_crud_delete2_Returning() 58 | { 59 | using var response = await test.Client.DeleteAsync("/api/crud-delete2/returning/?id=1"); 60 | var content = await response.Content.ReadAsStringAsync(); 61 | response.StatusCode.Should().Be(HttpStatusCode.OK); 62 | 63 | content.Should().Be(""" 64 | [ 65 | {"id":1,"name":"name1"} 66 | ] 67 | """ 68 | .Replace(" ", "") 69 | .Replace("\n", "") 70 | .Replace("\r", "") 71 | .Trim()); 72 | 73 | using var response2 = await test.Client.DeleteAsync("/api/crud-delete2/returning/"); 74 | var content2 = await response2.Content.ReadAsStringAsync(); 75 | response2.StatusCode.Should().Be(HttpStatusCode.OK); 76 | 77 | content2.Should().Be(""" 78 | [ 79 | {"id":2,"name":"name2"}, 80 | {"id":3,"name":"name3"}, 81 | {"id":4,"name":"name4"} 82 | ] 83 | """ 84 | .Replace(" ", "") 85 | .Replace("\n", "") 86 | .Replace("\r", "") 87 | .Trim()); 88 | } 89 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CrudTests/CrudInsertReturningTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CrudInsertReturningTests() 6 | { 7 | script.Append(""" 8 | create table crud_insert_returning1 ( 9 | id int not null generated always as identity, 10 | name text 11 | ); 12 | """); 13 | } 14 | } 15 | 16 | [Collection("TestFixture")] 17 | public class CrudInsertReturningTests(TestFixture test) 18 | { 19 | [Fact] 20 | public async Task Test_crud_insert_returning1_Name() 21 | { 22 | using var body = new StringContent("{\"name\":\"inserted1\"}", Encoding.UTF8, "application/json"); 23 | using var response = await test.Client.PutAsync("/api/crud-insert-returning1/returning/", body); 24 | var content = await response.Content.ReadAsStringAsync(); 25 | response.StatusCode.Should().Be(HttpStatusCode.OK); 26 | 27 | content.Should().Be(""" 28 | [ 29 | {"id":1,"name":"inserted1"} 30 | ] 31 | """ 32 | .Replace(" ", "") 33 | .Replace("\n", "") 34 | .Replace("\r", "") 35 | .Trim()); 36 | } 37 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CrudTests/CrudInsertTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CrudInsertTests() 6 | { 7 | script.Append(""" 8 | create table crud_insert1 ( 9 | id int, 10 | name text, 11 | some_date date not null, 12 | status boolean not null 13 | ); 14 | 15 | create table crud_insert2 ( 16 | id int, 17 | name text, 18 | some_date date not null, 19 | status boolean not null 20 | ); 21 | 22 | create table crud_insert3 ( 23 | id int not null generated always as identity, 24 | name text 25 | ); 26 | 27 | create table crud_insert4 ( 28 | name1 text null, 29 | name2 text not null 30 | ); 31 | """); 32 | } 33 | } 34 | 35 | [Collection("TestFixture")] 36 | public class CrudInsertTests(TestFixture test) 37 | { 38 | [Fact] 39 | public async Task Test_crud_insert1() 40 | { 41 | using var body = new StringContent("{\"id\":1,\"name\":\"name1\",\"someDate\":\"2024-02-22\",\"status\":true}", Encoding.UTF8, "application/json"); 42 | using var response = await test.Client.PutAsync("/api/crud-insert1/", body); 43 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 44 | 45 | using var getResponse = await test.Client.GetAsync("/api/crud-insert1/"); 46 | var content = await getResponse.Content.ReadAsStringAsync(); 47 | 48 | content.Should().Be(""" 49 | [ 50 | {"id":1,"name":"name1","someDate":"2024-02-22","status":true} 51 | ] 52 | """ 53 | .Replace(" ", "") 54 | .Replace("\n", "") 55 | .Replace("\r", "") 56 | .Trim()); 57 | } 58 | 59 | [Fact] 60 | public async Task Test_crud_insert2_DateAndStatus() 61 | { 62 | using var body = new StringContent("{\"someDate\":\"2024-02-23\",\"status\":true}", Encoding.UTF8, "application/json"); 63 | using var response = await test.Client.PutAsync("/api/crud-insert2/", body); 64 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 65 | 66 | using var getResponse = await test.Client.GetAsync("/api/crud-insert2/"); 67 | var content = await getResponse.Content.ReadAsStringAsync(); 68 | 69 | content.Should().Be(""" 70 | [ 71 | {"id":null,"name":null,"someDate":"2024-02-23","status":true} 72 | ] 73 | """ 74 | .Replace(" ", "") 75 | .Replace("\n", "") 76 | .Replace("\r", "") 77 | .Trim()); 78 | } 79 | 80 | [Fact] 81 | public async Task Test_crud_insert3() 82 | { 83 | using var body = new StringContent("{\"id\":1,\"name\":\"name1\"}", Encoding.UTF8, "application/json"); 84 | using var response = await test.Client.PutAsync("/api/crud-insert3/", body); 85 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 86 | 87 | using var getResponse = await test.Client.GetAsync("/api/crud-insert3/"); 88 | var content = await getResponse.Content.ReadAsStringAsync(); 89 | 90 | content.Should().Be(""" 91 | [ 92 | {"id":1,"name":"name1"} 93 | ] 94 | """ 95 | .Replace(" ", "") 96 | .Replace("\n", "") 97 | .Replace("\r", "") 98 | .Trim()); 99 | } 100 | 101 | [Fact] 102 | public async Task Test_crud_insert4() 103 | { 104 | using var body = new StringContent("{\"name1\":\"name1\"}", Encoding.UTF8, "application/json"); 105 | using var response = await test.Client.PutAsync("/api/crud-insert4/", body); 106 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 107 | 108 | using var body2 = new StringContent("{\"name1\":\"name1\",\"name2\":\"name2\"}", Encoding.UTF8, "application/json"); 109 | using var response2 = await test.Client.PutAsync("/api/crud-insert4/", body2); 110 | response2.StatusCode.Should().Be(HttpStatusCode.NoContent); 111 | 112 | using var getResponse = await test.Client.GetAsync("/api/crud-insert4/"); 113 | var content = await getResponse.Content.ReadAsStringAsync(); 114 | 115 | content.Should().Be(""" 116 | [ 117 | {"name1":"name1","name2":"name2"} 118 | ] 119 | """ 120 | .Replace(" ", "") 121 | .Replace("\n", "") 122 | .Replace("\r", "") 123 | .Trim()); 124 | } 125 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CrudTests/CrudUpdateReturningTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CrudUpdateReturningTests() 6 | { 7 | script.Append(""" 8 | create table crud_update_returning_tests ( 9 | id serial primary key, 10 | name text not null, 11 | some_date date not null, 12 | status boolean not null 13 | ); 14 | 15 | insert into crud_update_returning_tests 16 | (id, name, some_date, status) 17 | values 18 | (1, 'name1', '2024-01-01', true), 19 | (2, 'name2', '2024-01-20', false), 20 | (3, 'name3', '2024-01-25', true); 21 | """); 22 | } 23 | } 24 | 25 | [Collection("TestFixture")] 26 | public class CrudUpdateReturningTests(TestFixture test) 27 | { 28 | [Fact] 29 | public async Task Test_crud_update_returning_tests() 30 | { 31 | using var body = new StringContent("{\"id\":1,\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 32 | using var response = await test.Client.PostAsync("/api/crud-update-returning-tests/returning/", body); 33 | var content = await response.Content.ReadAsStringAsync(); 34 | response.StatusCode.Should().Be(HttpStatusCode.OK); 35 | 36 | content.Should().Be(""" 37 | [ 38 | {"id":1,"name":"updated1","someDate":"2024-01-01","status":false} 39 | ] 40 | """ 41 | .Replace(" ", "") 42 | .Replace("\n", "") 43 | .Replace("\r", "") 44 | .Trim()); 45 | } 46 | 47 | [Fact] 48 | public async Task Test_crud_update_returning_tests_NoneExistingParam() 49 | { 50 | using var body = new StringContent("{\"bla\":1,\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 51 | using var response = await test.Client.PostAsync("/api/crud-update-returning-tests/returning/", body); 52 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 53 | } 54 | 55 | [Fact] 56 | public async Task Test_crud_update_returning_tests_NoKeys() 57 | { 58 | using var body = new StringContent("{\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 59 | using var response = await test.Client.PostAsync("/api/crud-update-returning-tests/returning/", body); 60 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 61 | } 62 | 63 | [Fact] 64 | public async Task Test_crud_update_returning_tests_NoFields() 65 | { 66 | using var body = new StringContent("{\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 67 | using var response = await test.Client.PostAsync("/api/crud-update-returning-tests/returning/", body); 68 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 69 | } 70 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/CrudTests/CrudUpdateTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void CrudUpdateTests() 6 | { 7 | script.Append(""" 8 | create table crud_update_tests ( 9 | id serial primary key, 10 | name text not null, 11 | some_date date not null, 12 | status boolean not null 13 | ); 14 | 15 | insert into crud_update_tests 16 | (id, name, some_date, status) 17 | values 18 | (1, 'name1', '2024-01-01', true), 19 | (2, 'name2', '2024-01-20', false), 20 | (3, 'name3', '2024-01-25', true); 21 | """); 22 | } 23 | } 24 | 25 | [Collection("TestFixture")] 26 | public class CrudUpdateTests(TestFixture test) 27 | { 28 | [Fact] 29 | public async Task Test_crud_update_tests() 30 | { 31 | using var body = new StringContent("{\"id\":1,\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 32 | using var response = await test.Client.PostAsync("/api/crud-update-tests/", body); 33 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 34 | 35 | 36 | using var getResponse = await test.Client.GetAsync("/api/crud-update-tests/"); 37 | var content = await getResponse.Content.ReadAsStringAsync(); 38 | 39 | content.Should().Be(""" 40 | [ 41 | {"id":2,"name":"name2","someDate":"2024-01-20","status":false}, 42 | {"id":3,"name":"name3","someDate":"2024-01-25","status":true}, 43 | {"id":1,"name":"updated1","someDate":"2024-01-01","status":false} 44 | ] 45 | """ 46 | .Replace(" ", "") 47 | .Replace("\n", "") 48 | .Replace("\r", "") 49 | .Trim()); 50 | } 51 | 52 | [Fact] 53 | public async Task Test_crud_update_tests_NoneExistingParam() 54 | { 55 | using var body = new StringContent("{\"bla\":1,\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 56 | using var response = await test.Client.PostAsync("/api/crud-update-tests/", body); 57 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 58 | } 59 | 60 | [Fact] 61 | public async Task Test_crud_update_tests_NoKeys() 62 | { 63 | using var body = new StringContent("{\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 64 | using var response = await test.Client.PostAsync("/api/crud-update-tests/", body); 65 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 66 | } 67 | 68 | [Fact] 69 | public async Task Test_crud_update_tests_NoFields() 70 | { 71 | using var body = new StringContent("{\"name\":\"updated1\",\"status\":false}", Encoding.UTF8, "application/json"); 72 | using var response = await test.Client.PostAsync("/api/crud-update-tests/", body); 73 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 74 | } 75 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/EndingSlashTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void EndingSlashTests() 6 | { 7 | script.Append(@" 8 | create function ending_slash() 9 | returns text 10 | language sql 11 | volatile 12 | as $$ 13 | select 'ending_slash' 14 | $$; 15 | "); 16 | } 17 | } 18 | 19 | [Collection("TestFixture")] 20 | public class EndingSlashTests(TestFixture test) 21 | { 22 | [Fact] 23 | public async Task Test_ending_slash() 24 | { 25 | using var response = await test.Client.PostAsync("/api/ending-slash/", null); 26 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 27 | } 28 | 29 | [Fact] 30 | public async Task Test_ending_slash_NoSlash() 31 | { 32 | using var response = await test.Client.PostAsync("/api/ending-slash", null); 33 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 34 | } 35 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ErrorHandlingTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ErrorHandlingTests() 6 | { 7 | script.Append(@" 8 | create function raise_exception() 9 | returns text 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | raise exception 'Test exception'; 15 | return 'test'; 16 | end; 17 | $$; 18 | 19 | create function assert_failure_exception() 20 | returns text 21 | language plpgsql 22 | as 23 | $$ 24 | begin 25 | assert false, 'Test assert failure'; 26 | return 'test'; 27 | end; 28 | $$; 29 | 30 | create function division_by_zero_exception( 31 | _meta json = null 32 | ) 33 | returns text 34 | language plpgsql 35 | as 36 | $$ 37 | declare 38 | _result int = 1 / 0; 39 | begin 40 | return 'test'; 41 | end; 42 | $$; 43 | "); 44 | } 45 | } 46 | 47 | [Collection("TestFixture")] 48 | public class ErrorHandlingTests(TestFixture test) 49 | { 50 | [Fact] 51 | public async Task Test_raise_exception_test() 52 | { 53 | using var result = await test.Client.PostAsync("/api/raise-exception/", null); 54 | result.StatusCode.Should().Be(HttpStatusCode.BadRequest); 55 | var response = await result.Content.ReadAsStringAsync(); 56 | 57 | response.Should().Be("Test exception"); 58 | } 59 | 60 | [Fact] 61 | public async Task Test_assert_failure_exception_test() 62 | { 63 | using var result = await test.Client.PostAsync("/api/assert-failure-exception/", null); 64 | result.StatusCode.Should().Be(HttpStatusCode.BadRequest); 65 | var response = await result.Content.ReadAsStringAsync(); 66 | 67 | response.Should().Be("Test assert failure"); 68 | } 69 | 70 | [Fact] 71 | public async Task Test_division_by_zero_exception_test() 72 | { 73 | using var result = await test.Client.PostAsync("/api/division-by-zero-exception/", null); 74 | result.StatusCode.Should().Be(HttpStatusCode.InternalServerError); 75 | var response = await result.Content.ReadAsStringAsync(); 76 | 77 | response.Should().Be("22012: division by zero"); 78 | } 79 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/IdentNamesTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void IdentNamesTests() 6 | { 7 | script.Append(""" 8 | 9 | create function "select"("group" int, "order" int) 10 | returns table ( 11 | "from" int, 12 | "join" int 13 | ) 14 | language plpgsql 15 | as 16 | $$ 17 | begin 18 | return query 19 | select t.* 20 | from ( 21 | values 22 | ("group", "order") 23 | ) t; 24 | end; 25 | $$; 26 | 27 | """); 28 | } 29 | } 30 | 31 | [Collection("TestFixture")] 32 | public class IdentNamesTests(TestFixture test) 33 | { 34 | [Fact] 35 | public async Task Test_select_ident_names_function() 36 | { 37 | using var body = new StringContent("{\"group\": 1, \"order\": 2}", Encoding.UTF8); 38 | using var response = await test.Client.PostAsync("/api/select/", body); 39 | var content = await response.Content.ReadAsStringAsync(); 40 | 41 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 42 | response?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 43 | content.Should().Be("[{\"from\":1,\"join\":2}]"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /NpgsqlRestTests/MixinTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void MixinTests() 6 | { 7 | script.Append(@" 8 | create schema mixins; 9 | 10 | create table mixins.id ( 11 | id int generated always as identity primary key 12 | ); 13 | 14 | create table mixin_users ( 15 | like mixins.id including all, 16 | name text not null 17 | ); 18 | 19 | insert into mixin_users (id, name) 20 | overriding system value values 21 | (1, 'test1'), (2, 'test2'); 22 | 23 | create table mixins.audit ( 24 | created_by int not null references mixin_users(id), 25 | modified_by int not null references mixin_users(id), 26 | created_at timestamptz not null default now(), 27 | modified_at timestamptz not null default now() 28 | ); 29 | 30 | create table test_mixins ( 31 | like mixins.id including all, 32 | like mixins.audit including all 33 | ); 34 | 35 | insert into test_mixins (id, created_by, modified_by, created_at, modified_at) 36 | overriding system value 37 | values (1, 1, 1, '2024-01-01', '2024-01-01'), 38 | (2, 2, 2, '2024-01-02', '2024-01-02'); 39 | 40 | create or replace function get_audit_mixins_test( 41 | _p mixins.id 42 | ) 43 | returns table ( 44 | result mixins.audit 45 | ) 46 | language sql as 47 | $$ 48 | select row(t.*)::mixins.audit 49 | from ( 50 | select created_by, modified_by, created_at, modified_at from test_mixins 51 | ) t; 52 | $$; 53 | "); 54 | } 55 | } 56 | 57 | [Collection("TestFixture")] 58 | public class MixinTests(TestFixture test) 59 | { 60 | [Fact] 61 | public async Task Test_get_audit_mixins_test() 62 | { 63 | using var response = await test.Client.GetAsync($"/api/get-audit-mixins-test?pId=1"); 64 | var content = await response.Content.ReadAsStringAsync(); 65 | 66 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 67 | response?.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); 68 | content.Should().Be("[{\"createdBy\":1,\"modifiedBy\":1,\"createdAt\":\"2024-01-01T00:00:00+00\",\"modifiedAt\":\"2024-01-01T00:00:00+00\"},{\"createdBy\":2,\"modifiedBy\":2,\"createdAt\":\"2024-01-02T00:00:00+00\",\"modifiedAt\":\"2024-01-02T00:00:00+00\"}]"); 69 | } 70 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/NonPublicSchemaTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void NonPublicSchemaTests() 6 | { 7 | script.Append(@" 8 | create schema if not exists my_schema; 9 | 10 | create function my_schema.hello_world() 11 | returns text 12 | language sql 13 | as $$ 14 | select 'Hello World' 15 | $$; 16 | 17 | create function my_schema.case_return_text(_t text) 18 | returns text 19 | language plpgsql 20 | as 21 | $$ 22 | begin 23 | raise info '_t = %', _t; 24 | return _t; 25 | end; 26 | $$; 27 | "); 28 | } 29 | } 30 | 31 | [Collection("TestFixture")] 32 | public class NonPublicSchemaTests(TestFixture test) 33 | { 34 | [Fact] 35 | public async Task Test_my_schema__hello_world() 36 | { 37 | using var result = await test.Client.PostAsync("/api/my-schema/hello-world/", null); 38 | var response = await result.Content.ReadAsStringAsync(); 39 | 40 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 41 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 42 | response.Should().Be("Hello World"); 43 | } 44 | 45 | [Fact] 46 | public async Task Test_my_schema__case_return_text() 47 | { 48 | using var content = new StringContent("{\"t\":\"Hello World\"}", Encoding.UTF8, "application/json"); 49 | using var result = await test.Client.PostAsync("/api/my-schema/case-return-text/", content); 50 | var response = await result.Content.ReadAsStringAsync(); 51 | 52 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 53 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 54 | response.Should().Be("Hello World"); 55 | } 56 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/NotDotnetCompliantTypeParams.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void NotDotnetCompliantTypeParams() 6 | { 7 | script.Append(@" 8 | create function case_jsonpath_param( 9 | _jsonpath jsonpath 10 | ) 11 | returns text 12 | language plpgsql 13 | as 14 | $$ 15 | begin 16 | return _jsonpath::text; 17 | end; 18 | $$; 19 | "); 20 | } 21 | } 22 | 23 | 24 | [Collection("TestFixture")] 25 | public class NotDotnetCompliantTypeParams(TestFixture test) 26 | { 27 | [Fact] 28 | public async Task Test_case_jsonpath_param_BadRequest() 29 | { 30 | string body = """ 31 | { 32 | "jsonpath": "XXX" 33 | } 34 | """; 35 | using var content = new StringContent(body, Encoding.UTF8, "application/json"); 36 | using var result = await test.Client.PostAsync("/api/case-jsonpath-param/", content); 37 | var response = await result.Content.ReadAsStringAsync(); 38 | 39 | result?.StatusCode.Should().Be(HttpStatusCode.BadRequest); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /NpgsqlRestTests/NpgsqlRestTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | true 11 | 12 | false 13 | NpgsqlRestTests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /NpgsqlRestTests/ParamTests/NoneExistingParamTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void NoneExistingParamTests() 6 | { 7 | script.Append(""" 8 | create function get_none_existing_params(_p1 int, _p2 int) 9 | returns text 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return 'OK'; 15 | end; 16 | $$; 17 | 18 | create function post_none_existing_params(_p1 int, _p2 int) 19 | returns text 20 | language plpgsql 21 | as 22 | $$ 23 | begin 24 | return 'OK'; 25 | end; 26 | $$; 27 | """); 28 | } 29 | } 30 | 31 | [Collection("TestFixture")] 32 | public class NoneExistingParamTests(TestFixture test) 33 | { 34 | [Fact] 35 | public async Task Test_none_existing_params__OK() 36 | { 37 | using var response = await test.Client.GetAsync("/api/get-none-existing-params/?p1=1&p2=2"); 38 | response.StatusCode.Should().Be(HttpStatusCode.OK); 39 | } 40 | 41 | [Fact] 42 | public async Task Test_post_none_existing_params__OK() 43 | { 44 | using var body = new StringContent("{\"p1\":1,\"p2\":2}", Encoding.UTF8, "application/json"); 45 | using var response = await test.Client.PostAsync("/api/post-none-existing-params/", body); 46 | response.StatusCode.Should().Be(HttpStatusCode.OK); 47 | } 48 | 49 | [Fact] 50 | public async Task Test_none_existing_params__NotFound1() 51 | { 52 | using var response = await test.Client.GetAsync("/api/get-none-existing-params/?x1=1&x2=2"); 53 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 54 | } 55 | 56 | [Fact] 57 | public async Task Test_post_none_existing_params__NotFound1() 58 | { 59 | using var body = new StringContent("{\"x1\":1,\"x2\":2}", Encoding.UTF8, "application/json"); 60 | using var response = await test.Client.PostAsync("/api/post-none-existing-params/", body); 61 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 62 | } 63 | 64 | [Fact] 65 | public async Task Test_none_existing_params__NotFound2() 66 | { 67 | using var response = await test.Client.GetAsync("/api/get-none-existing-params/?x1=1&p2=2"); 68 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 69 | } 70 | 71 | [Fact] 72 | public async Task Test_post_none_existing_params__NotFound2() 73 | { 74 | using var body = new StringContent("{\"x1\":1,\"p2\":2}", Encoding.UTF8, "application/json"); 75 | using var response = await test.Client.PostAsync("/api/post-none-existing-params/", body); 76 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 77 | } 78 | 79 | [Fact] 80 | public async Task Test_none_existing_params__NotFound3() 81 | { 82 | using var response = await test.Client.GetAsync("/api/get-none-existing-params/?p1=1&x2=2"); 83 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 84 | } 85 | 86 | [Fact] 87 | public async Task Test_post_none_existing_params__NotFound3() 88 | { 89 | using var body = new StringContent("{\"p1\":1,\"x2\":2}", Encoding.UTF8, "application/json"); 90 | using var response = await test.Client.PostAsync("/api/post-none-existing-params/", body); 91 | response.StatusCode.Should().Be(HttpStatusCode.NotFound); 92 | } 93 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ParamTests/OverloadQueryStringTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void OverloadQueryStringTests() 6 | { 7 | script.Append(@" 8 | create function case_get_overload() 9 | returns text 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return 'no params'; 15 | end; 16 | $$; 17 | 18 | create function case_get_overload(_i int) 19 | returns text 20 | language plpgsql 21 | as 22 | $$ 23 | begin 24 | return '1 param'; 25 | end; 26 | $$; 27 | 28 | create function case_get_overload(_i int, _t text) 29 | returns text 30 | language plpgsql 31 | as 32 | $$ 33 | begin 34 | return '2 params'; 35 | end; 36 | $$; 37 | 38 | create function case_get_overload(_i int, _t text, _b boolean) 39 | returns text 40 | language plpgsql 41 | as 42 | $$ 43 | begin 44 | return '3 params'; 45 | end; 46 | $$; 47 | "); 48 | } 49 | } 50 | 51 | [Collection("TestFixture")] 52 | public class OverloadQueryStringTests(TestFixture test) 53 | { 54 | [Fact] 55 | public async Task Test_case_get_overload_NoParams() 56 | { 57 | using var result = await test.Client.GetAsync("/api/case-get-overload/"); 58 | var response = await result.Content.ReadAsStringAsync(); 59 | 60 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 61 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 62 | response.Should().Be("no params"); 63 | } 64 | 65 | [Fact] 66 | public async Task Test_case_get_overload_OneParam() 67 | { 68 | var query = new QueryBuilder { { "i", "1" } }; 69 | using var result = await test.Client.GetAsync($"/api/case-get-overload/{query}"); 70 | var response = await result.Content.ReadAsStringAsync(); 71 | 72 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 73 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 74 | response.Should().Be("1 param"); 75 | } 76 | 77 | [Fact] 78 | public async Task Test_case_get_overload_TwoParams() 79 | { 80 | var query = new QueryBuilder { { "i", "1" }, { "t", "ABC" } }; 81 | using var result = await test.Client.GetAsync($"/api/case-get-overload/{query}"); 82 | var response = await result.Content.ReadAsStringAsync(); 83 | 84 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 85 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 86 | response.Should().Be("2 params"); 87 | } 88 | 89 | [Fact] 90 | public async Task Test_case_get_overload_ThreeParams() 91 | { 92 | var query = new QueryBuilder { { "i", "1" }, { "t", "ABC" }, { "b", "true" } }; 93 | using var result = await test.Client.GetAsync($"/api/case-get-overload/{query}"); 94 | var response = await result.Content.ReadAsStringAsync(); 95 | 96 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 97 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 98 | response.Should().Be("3 params"); 99 | } 100 | 101 | [Fact] 102 | public async Task Test_case_get_overload_WrongParams() 103 | { 104 | var query = new QueryBuilder { { "i", "1" }, { "t", "ABC" }, { "X", "true" } }; 105 | using var result = await test.Client.GetAsync($"/api/case-get-overload/{query}"); 106 | var response = await result.Content.ReadAsStringAsync(); 107 | 108 | result?.StatusCode.Should().Be(HttpStatusCode.NotFound); 109 | } 110 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ParamTests/OverloadTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void OverloadTests() 6 | { 7 | script.Append(@" 8 | create function case_overload() 9 | returns text 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return 'no params'; 15 | end; 16 | $$; 17 | 18 | create function case_overload(_i int) 19 | returns text 20 | language plpgsql 21 | as 22 | $$ 23 | begin 24 | return '1 param'; 25 | end; 26 | $$; 27 | 28 | create function case_overload(_i int, _t text) 29 | returns text 30 | language plpgsql 31 | as 32 | $$ 33 | begin 34 | return '2 params'; 35 | end; 36 | $$; 37 | 38 | create function case_overload(_i int, _t text, _b boolean) 39 | returns text 40 | language plpgsql 41 | as 42 | $$ 43 | begin 44 | return '3 params'; 45 | end; 46 | $$; 47 | "); 48 | } 49 | } 50 | 51 | [Collection("TestFixture")] 52 | public class OverloadTests(TestFixture test) 53 | { 54 | [Fact] 55 | public async Task Test_Overload_NoParams1() 56 | { 57 | using var result = await test.Client.PostAsync("/api/case-overload/", null); 58 | var response = await result.Content.ReadAsStringAsync(); 59 | 60 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 61 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 62 | response.Should().Be("no params"); 63 | } 64 | 65 | [Fact] 66 | public async Task Test_case_overload_NoParams2() 67 | { 68 | using var content = new StringContent("{}", Encoding.UTF8, "application/json"); 69 | using var result = await test.Client.PostAsync("/api/case-overload/", content); 70 | var response = await result.Content.ReadAsStringAsync(); 71 | 72 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 73 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 74 | response.Should().Be("no params"); 75 | } 76 | 77 | [Fact] 78 | public async Task Test_case_overload_OneParam() 79 | { 80 | using var content = new StringContent("{\"i\": 1}", Encoding.UTF8, "application/json"); 81 | using var result = await test.Client.PostAsync("/api/case-overload/", content); 82 | var response = await result.Content.ReadAsStringAsync(); 83 | 84 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 85 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 86 | response.Should().Be("1 param"); 87 | } 88 | 89 | [Fact] 90 | public async Task Test_case_overload_Two_arams() 91 | { 92 | using var content = new StringContent("{\"i\": 1, \"t\": \"ABC\"}", Encoding.UTF8, "application/json"); 93 | using var result = await test.Client.PostAsync("/api/case-overload/", content); 94 | var response = await result.Content.ReadAsStringAsync(); 95 | 96 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 97 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 98 | response.Should().Be("2 params"); 99 | } 100 | 101 | [Fact] 102 | public async Task Test_case_overload_ThreeParams() 103 | { 104 | using var content = new StringContent("{\"i\": 1, \"t\": \"ABC\", \"b\": true}", Encoding.UTF8, "application/json"); 105 | using var result = await test.Client.PostAsync("/api/case-overload/", content); 106 | var response = await result.Content.ReadAsStringAsync(); 107 | 108 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 109 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 110 | response.Should().Be("3 params"); 111 | } 112 | 113 | [Fact] 114 | public async Task Test_case_overload_WrongParams() 115 | { 116 | using var content = new StringContent("{\"i\": 1, \"t\": \"ABC\", \"X\": true}", Encoding.UTF8, "application/json"); 117 | using var result = await test.Client.PostAsync("/api/case-overload/", content); 118 | var response = await result.Content.ReadAsStringAsync(); 119 | 120 | result?.StatusCode.Should().Be(HttpStatusCode.NotFound); 121 | } 122 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ParamTests/ReturnIntTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnIntTests() 6 | { 7 | script.Append(@" 8 | create function case_return_int(_i int) 9 | returns int 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | raise info '_i = %', _i; 15 | return _i; 16 | end; 17 | $$; 18 | 19 | create function case_get_int(_i int) 20 | returns int 21 | language plpgsql 22 | as 23 | $$ 24 | begin 25 | raise info '_i = %', _i; 26 | return _i; 27 | end; 28 | $$; 29 | "); 30 | } 31 | } 32 | 33 | [Collection("TestFixture")] 34 | public class ReturnIntTests(TestFixture test) 35 | { 36 | [Fact] 37 | public async Task Test_case_return_int() 38 | { 39 | using var content = new StringContent("{\"i\":999}", Encoding.UTF8, "application/json"); 40 | using var result = await test.Client.PostAsync("/api/case-return-int/", content); 41 | var response = await result.Content.ReadAsStringAsync(); 42 | 43 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 44 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 45 | response.Should().Be("999"); 46 | } 47 | 48 | [Fact] 49 | public async Task Test_case_return_int_WrongParameter() 50 | { 51 | using var content = new StringContent("{\"x\":666}", Encoding.UTF8, "application/json"); 52 | using var result = await test.Client.PostAsync("/api/case-return-int/", content); 53 | 54 | result?.StatusCode.Should().Be(HttpStatusCode.NotFound); 55 | } 56 | 57 | [Fact] 58 | public async Task Test_case_get_int() 59 | { 60 | using var result = await test.Client.GetAsync("/api/case-get-int/?i=999"); 61 | var response = await result.Content.ReadAsStringAsync(); 62 | 63 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 64 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 65 | response.Should().Be("999"); 66 | } 67 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ParamTests/UnnamedParamsTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void UnnamedParamsTests() 6 | { 7 | script.Append(@" 8 | create function case_get_unnamed_int(int) 9 | returns int 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | raise info '_i = %', $1; 15 | return $1; 16 | end; 17 | $$; 18 | 19 | create function case_return_unnamed_int(int) 20 | returns int 21 | language plpgsql 22 | as 23 | $$ 24 | begin 25 | raise info '_i = %', $1; 26 | return $1; 27 | end; 28 | $$; 29 | "); 30 | } 31 | } 32 | 33 | [Collection("TestFixture")] 34 | public class UnnamedParamsTests(TestFixture test) 35 | { 36 | [Fact] 37 | public async Task Test_case_get_unnamed_int() 38 | { 39 | using var result = await test.Client.GetAsync("/api/case-get-unnamed-int/?$1=999"); 40 | var response = await result.Content.ReadAsStringAsync(); 41 | 42 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 43 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 44 | response.Should().Be("999"); 45 | } 46 | 47 | [Fact] 48 | public async Task Test_case_return_unnamed_int() 49 | { 50 | using var content = new StringContent("{\"$1\":999}", Encoding.UTF8, "application/json"); 51 | using var result = await test.Client.PostAsync("/api/case-return-unnamed-int/", content); 52 | var response = await result.Content.ReadAsStringAsync(); 53 | 54 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 55 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 56 | response.Should().Be("999"); 57 | } 58 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ParamTests/VariadicParamTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void VariadicParamTests() 6 | { 7 | script.Append(@" 8 | create function variadic_param_plus_one(variadic v int[]) 9 | returns int[] 10 | language sql 11 | as 12 | $$ 13 | select array_agg(n + 1) from unnest($1) AS n; 14 | $$; 15 | "); 16 | } 17 | } 18 | 19 | [Collection("TestFixture")] 20 | public class VariadicParamTests(TestFixture test) 21 | { 22 | [Fact] 23 | public async Task Test_variadic_param_plus_one() 24 | { 25 | using var body = new StringContent("{\"v\": [1,2,3,4]}", Encoding.UTF8); 26 | using var response = await test.Client.PostAsync("/api/variadic-param-plus-one", body); 27 | var content = await response.Content.ReadAsStringAsync(); 28 | 29 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 30 | content.Should().Be("[2,3,4,5]"); 31 | } 32 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ParserTests/PatternMatcherTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests.ParserTests; 2 | 3 | public class PatternMatcherTests 4 | { 5 | [Theory] 6 | [InlineData("", "", false)] 7 | [InlineData("test", "", false)] 8 | [InlineData("", "test", false)] 9 | [InlineData(null, "test", false)] 10 | [InlineData("test", null, false)] 11 | [InlineData(null, null, false)] 12 | public void EdgeCases_EmptyAndNull_ReturnFalse(string? name, string? pattern, bool expected) 13 | { 14 | Parser.IsPatternMatch(name!, pattern!).Should().Be(expected); 15 | } 16 | 17 | [Theory] 18 | [InlineData("test", "test", true)] 19 | [InlineData("test", "TEST", true)] 20 | [InlineData("a", "b", false)] 21 | [InlineData("abc", "abcd", false)] 22 | public void ExactMatches_WithoutWildcards(string name, string pattern, bool expected) 23 | { 24 | Parser.IsPatternMatch(name, pattern).Should().Be(expected); 25 | } 26 | 27 | [Theory] 28 | [InlineData("test.txt", "*.txt", true)] 29 | [InlineData("test.doc", "*.txt", false)] 30 | [InlineData("file", "*", true)] 31 | [InlineData("", "*", false)] 32 | [InlineData("abc", "a*", true)] 33 | [InlineData("abc", "*c", true)] 34 | [InlineData("abc", "a*c", true)] 35 | [InlineData("abcdef", "*d*f", true)] 36 | [InlineData("abc", "*d", false)] 37 | public void StarWildcard_MatchesCorrectly(string name, string pattern, bool expected) 38 | { 39 | Parser.IsPatternMatch(name, pattern).Should().Be(expected); 40 | } 41 | 42 | [Theory] 43 | [InlineData("test", "t?st", true)] 44 | [InlineData("test", "te?t", true)] 45 | [InlineData("test", "????", true)] 46 | [InlineData("test", "tes?", true)] 47 | [InlineData("test", "?est", true)] 48 | [InlineData("abc", "a?c?", false)] 49 | [InlineData("abc", "??", false)] 50 | [InlineData("abc", "a?d", false)] 51 | public void QuestionMarkWildcard_MatchesCorrectly(string name, string pattern, bool expected) 52 | { 53 | Parser.IsPatternMatch(name, pattern).Should().Be(expected); 54 | } 55 | 56 | [Theory] 57 | [InlineData("testfile.txt", "t*t", true)] 58 | [InlineData("abcde", "a*c?e", true)] 59 | [InlineData("abcde", "*?e", true)] 60 | [InlineData("x", "*?*", true)] 61 | [InlineData("abcdef", "a*d?f", true)] 62 | [InlineData("a", "**", true)] 63 | [InlineData("abc", "a**c", true)] 64 | public void CombinedWildcards_MatchesCorrectly(string name, string pattern, bool expected) 65 | { 66 | Parser.IsPatternMatch(name, pattern).Should().Be(expected); 67 | } 68 | 69 | [Theory] 70 | [InlineData("verylongfilename.txt", "*.txt", true)] 71 | [InlineData("a", "************************a", true)] 72 | [InlineData("abc", "???", true)] 73 | [InlineData("special@#$%.txt", "*@#$%.txt", true)] 74 | public void ExtremeCases_MatchesCorrectly(string name, string pattern, bool expected) 75 | { 76 | Parser.IsPatternMatch(name, pattern).Should().Be(expected); 77 | } 78 | 79 | [Fact] 80 | public void PerformanceTest_LargeInput_DoesNotStackOverflow() 81 | { 82 | string largeName = new('a', 10000); 83 | string largePattern = "*" + new string('?', 9999); 84 | Assert.True(Parser.IsPatternMatch(largeName, largePattern)); 85 | } 86 | 87 | [Theory] 88 | [InlineData("test", "TEST", true)] 89 | [InlineData("Test", "TEST", true)] 90 | [InlineData("test", "tEsT", true)] 91 | [InlineData("TEST", "t?st", true)] 92 | [InlineData("TEST", "t*st", true)] 93 | [InlineData("TEST", "*st", true)] 94 | public void CaseInsensitive_MatchesCorrectly(string name, string pattern, bool expected) 95 | { 96 | Parser.IsPatternMatch(name, pattern).Should().Be(expected); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /NpgsqlRestTests/QuotedJsonTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void QuotedJsonTests() 6 | { 7 | script.Append( 8 | """ 9 | create function case_quoted_texts() 10 | returns setof text 11 | language sql 12 | as 13 | $$ 14 | select * from (values ('aaa'), ('a''a'), ('a"a'), ('a""a')) sub (a) 15 | $$; 16 | 17 | create function case_quoted_text_table() 18 | returns table(t text) 19 | language sql 20 | as 21 | $$ 22 | select * from (values 23 | ('aaa'), 24 | ('a''a'), 25 | ('a"a'), 26 | ('a""a') 27 | ) sub (a) 28 | $$; 29 | """); 30 | } 31 | } 32 | 33 | [Collection("TestFixture")] 34 | public class QuotedJsonTests(TestFixture test) 35 | { 36 | [Fact] 37 | public async Task Test_case_quoted_texts() 38 | { 39 | using var result = await test.Client.PostAsync("/api/case-quoted-texts/", null); 40 | var response = await result.Content.ReadAsStringAsync(); 41 | 42 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 43 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 44 | response.Should().Be("[\"aaa\",\"a'a\",\"a\\\"a\",\"a\\\"\\\"a\"]"); 45 | } 46 | 47 | [Fact] 48 | public async Task Test_case_quoted_text_table() 49 | { 50 | using var result = await test.Client.PostAsync("/api/case-quoted-text-table/", null); 51 | var response = await result.Content.ReadAsStringAsync(); 52 | 53 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 54 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 55 | response.Should().Be("[{\"t\":\"aaa\"},{\"t\":\"a'a\"},{\"t\":\"a\\\"a\"},{\"t\":\"a\\\"\\\"a\"}]"); 56 | } 57 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/RawContentTests/RawDownloadTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void RawDownloadTests() 6 | { 7 | script.Append( 8 | """ 9 | create function header_template_response1(_type text, _file text) 10 | returns table(n numeric, d timestamp, b boolean, t text) 11 | language sql 12 | as 13 | $$ 14 | select sub.* 15 | from ( 16 | values 17 | (123, '2024-01-01'::timestamp, true, 'some text'), 18 | (456, '2024-12-31'::timestamp, false, 'another text') 19 | ) 20 | sub (n, d, b, t) 21 | $$; 22 | 23 | comment on function header_template_response1(text, text) is ' 24 | raw 25 | separator , 26 | new_line \n 27 | Content-Type: {_type} 28 | Content-Disposition: attachment; filename={_file} 29 | '; 30 | 31 | create function header_template_response2(_type text, _file text) 32 | returns table(n numeric, d timestamp, b boolean, t text) 33 | language sql 34 | as 35 | $$ 36 | select sub.* 37 | from ( 38 | values 39 | (123, '2024-01-01'::timestamp, true, 'some text'), 40 | (456, '2024-12-31'::timestamp, false, 'another text') 41 | ) 42 | sub (n, d, b, t) 43 | $$; 44 | 45 | comment on function header_template_response2(text, text) is ' 46 | raw 47 | separator , 48 | new_line \n 49 | Content-Type: {type} 50 | Content-Disposition: attachment; filename={file} 51 | '; 52 | """); 53 | } 54 | } 55 | 56 | [Collection("TestFixture")] 57 | public class RawDownloadTests(TestFixture test) 58 | { 59 | [Fact] 60 | public async Task Test_header_template_response1() 61 | { 62 | string body = """ 63 | { 64 | "type": "text/csv", 65 | "file": "test.csv" 66 | } 67 | """; 68 | using var content = new StringContent(body, Encoding.UTF8, "application/json"); 69 | 70 | using var result = await test.Client.PostAsync("/api/header-template-response1/", content); 71 | var response = await result.Content.ReadAsStringAsync(); 72 | 73 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 74 | 75 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 76 | result.Content.Headers.ContentType.MediaType.Should().Be("text/csv"); 77 | result.Content.Headers.ContentDisposition.FileName.Should().Be("test.csv"); 78 | #pragma warning restore CS8602 // Dereference of a possibly null reference. 79 | response.Should().Be(string.Concat( 80 | "123,\"2024-01-01 00:00:00\",t,\"some text\"", 81 | "\n", 82 | "456,\"2024-12-31 00:00:00\",f,\"another text\"")); 83 | } 84 | 85 | [Fact] 86 | public async Task Test_header_template_response2() 87 | { 88 | string body = """ 89 | { 90 | "type": "text/csv", 91 | "file": "test.csv" 92 | } 93 | """; 94 | using var content = new StringContent(body, Encoding.UTF8, "application/json"); 95 | 96 | using var result = await test.Client.PostAsync("/api/header-template-response2/", content); 97 | var response = await result.Content.ReadAsStringAsync(); 98 | 99 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 100 | 101 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 102 | result.Content.Headers.ContentType.MediaType.Should().Be("text/csv"); 103 | result.Content.Headers.ContentDisposition.FileName.Should().Be("test.csv"); 104 | #pragma warning restore CS8602 // Dereference of a possibly null reference. 105 | response.Should().Be(string.Concat( 106 | "123,\"2024-01-01 00:00:00\",t,\"some text\"", 107 | "\n", 108 | "456,\"2024-12-31 00:00:00\",f,\"another text\"")); 109 | } 110 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnBooleanTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnBooleanTests() 6 | { 7 | script.Append(@" 8 | create function return_true() 9 | returns boolean 10 | language sql 11 | as $$ 12 | select true; 13 | $$; 14 | 15 | create function return_false() 16 | returns boolean 17 | language sql 18 | as $$ 19 | select false; 20 | $$; 21 | "); 22 | } 23 | } 24 | 25 | [Collection("TestFixture")] 26 | public class ReturnBooleanTests(TestFixture test) 27 | { 28 | [Fact] 29 | public async Task Test_return_true() 30 | { 31 | using var result = await test.Client.PostAsync("/api/return-true/", null); 32 | var response = await result.Content.ReadAsStringAsync(); 33 | 34 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 35 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 36 | response.Should().Be("t"); 37 | } 38 | 39 | [Fact] 40 | public async Task Test_return_false() 41 | { 42 | using var result = await test.Client.PostAsync("/api/return-false/", null); 43 | var response = await result.Content.ReadAsStringAsync(); 44 | 45 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 46 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 47 | response.Should().Be("f"); 48 | } 49 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnJsonTests.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 2 | namespace NpgsqlRestTests; 3 | 4 | public static partial class Database 5 | { 6 | public static void ReturnJsonTests() 7 | { 8 | script.Append(@" 9 | create function case_return_json(_json json) 10 | returns json 11 | language plpgsql 12 | as 13 | $$ 14 | begin 15 | raise info '_json = %', _json; 16 | return _json; 17 | end; 18 | $$; 19 | "); 20 | } 21 | } 22 | 23 | [Collection("TestFixture")] 24 | public class ReturnJsonTests(TestFixture test) 25 | { 26 | [Fact] 27 | public async Task Test_case_return_json() 28 | { 29 | using var content = new StringContent("{\"json\": {\"a\": 1, \"b\": \"c\"}}", Encoding.UTF8, "application/json"); 30 | using var result = await test.Client.PostAsync("/api/case-return-json/", content); 31 | var response = await result.Content.ReadAsStringAsync(); 32 | 33 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 34 | result.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 35 | 36 | var node = JsonNode.Parse(response); 37 | node["a"].ToJsonString().Should().Be("1"); 38 | node["b"].ToJsonString().Should().Be("\"c\""); 39 | } 40 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnLongTableTests.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 2 | namespace NpgsqlRestTests; 3 | 4 | public static partial class Database 5 | { 6 | public static void ReturnLongTableTests() 7 | { 8 | script.Append(""" 9 | create function case_get_long_table1(_records int) 10 | returns table ( 11 | int_field int, 12 | text_field text 13 | ) 14 | language sql 15 | as 16 | $_$ 17 | select 18 | i, i::text 19 | from 20 | generate_series(1, _records) as i; 21 | $_$; 22 | """); 23 | } 24 | } 25 | 26 | 27 | [Collection("TestFixture")] 28 | public class ReturnLongTableTests(TestFixture test) 29 | { 30 | [Fact] 31 | public async Task Test_case_get_long_table1_return10() 32 | { 33 | using var result = await test.Client.GetAsync("/api/case-get-long-table1/?records=10"); 34 | var response = await result.Content.ReadAsStringAsync(); 35 | 36 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 37 | result.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 38 | 39 | var array = JsonNode.Parse(response).AsArray(); 40 | array.Count.Should().Be(10); 41 | } 42 | 43 | [Fact] 44 | public async Task Test_case_get_long_table1_return25() 45 | { 46 | using var result = await test.Client.GetAsync("/api/case-get-long-table1/?records=25"); 47 | var response = await result.Content.ReadAsStringAsync(); 48 | 49 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 50 | result.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 51 | 52 | var array = JsonNode.Parse(response).AsArray(); 53 | array.Count.Should().Be(25); 54 | } 55 | 56 | [Fact] 57 | public async Task Test_case_get_long_table1_return75() 58 | { 59 | using var result = await test.Client.GetAsync("/api/case-get-long-table1/?records=75"); 60 | var response = await result.Content.ReadAsStringAsync(); 61 | 62 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 63 | result.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 64 | 65 | var array = JsonNode.Parse(response).AsArray(); 66 | array.Count.Should().Be(75); 67 | } 68 | 69 | [Fact] 70 | public async Task Test_case_get_long_table1_return0() 71 | { 72 | using var result = await test.Client.GetAsync("/api/case-get-long-table1/?records=0"); 73 | var response = await result.Content.ReadAsStringAsync(); 74 | 75 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 76 | result.Content.Headers.ContentType.MediaType.Should().Be("application/json"); 77 | 78 | var array = JsonNode.Parse(response).AsArray(); 79 | array.Count.Should().Be(0); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnSetOfBoolTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnSetOfBoolTests() 6 | { 7 | script.Append(@" 8 | create function case_return_setof_bool() 9 | returns setof boolean 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return query select j from ( 15 | values (true), (false), (true), (false) 16 | ) t(j); 17 | end; 18 | $$; 19 | "); 20 | } 21 | } 22 | 23 | [Collection("TestFixture")] 24 | public class ReturnSetOfBoolTests(TestFixture test) 25 | { 26 | [Fact] 27 | public async Task Test_case_return_setof_bool() 28 | { 29 | using var result = await test.Client.PostAsync("/api/case-return-setof-bool/", null); 30 | var response = await result.Content.ReadAsStringAsync(); 31 | 32 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 33 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 34 | response.Should().Be("[true,false,true,false]"); 35 | } 36 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnSetOfIntTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnSetOfIntTests() 6 | { 7 | script.Append(@" 8 | create function case_return_setof_int() 9 | returns setof int 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return query select i from (values (1), (2), (3)) t(i); 15 | end; 16 | $$; 17 | "); 18 | } 19 | } 20 | 21 | [Collection("TestFixture")] 22 | public class ReturnSetOfIntTests(TestFixture test) 23 | { 24 | [Fact] 25 | public async Task Test_case_return_setof_int() 26 | { 27 | using var result = await test.Client.PostAsync("/api/case-return-setof-int/", null); 28 | var response = await result.Content.ReadAsStringAsync(); 29 | 30 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 31 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 32 | response.Should().Be("[1,2,3]"); 33 | } 34 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnSetOfJsonTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnSetOfJsonTests() 6 | { 7 | script.Append(@" 8 | create function case_return_setof_json() 9 | returns setof json 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return query select j from ( 15 | values 16 | (json_build_object('A', 1)), 17 | (json_build_object('B', 'XY')), 18 | (json_build_object('C', true)), 19 | (json_build_object('D', null)) 20 | ) t(j); 21 | end; 22 | $$; 23 | "); 24 | } 25 | } 26 | 27 | [Collection("TestFixture")] 28 | public class ReturnSetOfJsonTests(TestFixture test) 29 | { 30 | [Fact] 31 | public async Task Test_case_return_setof_json() 32 | { 33 | using var result = await test.Client.PostAsync("/api/case-return-setof-json/", null); 34 | var response = await result.Content.ReadAsStringAsync(); 35 | 36 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 37 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 38 | response.Should().Be("[{\"A\" : 1},{\"B\" : \"XY\"},{\"C\" : true},{\"D\" : null}]"); 39 | } 40 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnSetOfTextTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnSetOfTextTests() 6 | { 7 | script.Append(@" 8 | create function case_return_setof_text() 9 | returns setof text 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | return query select i from (values ('ABC'), ('XYZ'), ('IJN')) t(i); 15 | end; 16 | $$; 17 | "); 18 | } 19 | } 20 | 21 | [Collection("TestFixture")] 22 | public class ReturnSetOfTextTests(TestFixture test) 23 | { 24 | [Fact] 25 | public async Task Test_case_return_setof_text() 26 | { 27 | using var result = await test.Client.PostAsync("/api/case-return-setof-text/", null); 28 | var response = await result.Content.ReadAsStringAsync(); 29 | 30 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 31 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 32 | response.Should().Be("[\"ABC\",\"XYZ\",\"IJN\"]"); 33 | } 34 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/ReturnTests/ReturnTextTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void ReturnTextTests() 6 | { 7 | script.Append(@" 8 | create function hello_world() 9 | returns text 10 | language sql 11 | as $$ 12 | select 'Hello World' 13 | $$; 14 | 15 | create function case_return_text(_t text) 16 | returns text 17 | language plpgsql 18 | as 19 | $$ 20 | begin 21 | raise info '_t = %', _t; 22 | return _t; 23 | end; 24 | $$; 25 | "); 26 | } 27 | } 28 | 29 | [Collection("TestFixture")] 30 | public class ReturnTextTests(TestFixture test) 31 | { 32 | [Fact] 33 | public async Task Test_hello_world() 34 | { 35 | using var result = await test.Client.PostAsync("/api/hello-world/", null); 36 | var response = await result.Content.ReadAsStringAsync(); 37 | 38 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 39 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 40 | response.Should().Be("Hello World"); 41 | } 42 | 43 | [Fact] 44 | public async Task Test_case_return_text() 45 | { 46 | using var content = new StringContent("{\"t\":\"Hello World\"}", Encoding.UTF8, "application/json"); 47 | using var result = await test.Client.PostAsync("/api/case-return-text/", content); 48 | var response = await result.Content.ReadAsStringAsync(); 49 | 50 | result?.StatusCode.Should().Be(HttpStatusCode.OK); 51 | result?.Content?.Headers?.ContentType?.MediaType.Should().Be("text/plain"); 52 | response.Should().Be("Hello World"); 53 | } 54 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/SetofRecordTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void SetofRecordTests() 6 | { 7 | script.Append(""""" 8 | create function get_test_records() 9 | returns setof record 10 | language sql 11 | as 12 | $$ 13 | select * from ( 14 | values 15 | (1, true, 'one'), 16 | (2, false, 'two'), 17 | (3, null, 'three'), 18 | (null, true, 'foo,bar'), 19 | (null, null, 'foo"bar'), 20 | (null, null, null), 21 | (1, null, 't'), 22 | (1, true, null), 23 | (null, null, '"foo"bar"'), 24 | (null, null, 'foo""bar'), 25 | (null, null, 'foo""""bar'), 26 | (null, null, 'foo"",""bar'), 27 | (null, null, 'foo\bar'), 28 | (null, null, 'foo/bar'), 29 | (null, null, E'foo\nbar') 30 | ) v; 31 | $$; 32 | """""); 33 | } 34 | } 35 | 36 | 37 | [Collection("TestFixture")] 38 | public class SetofRecordTests(TestFixture test) 39 | { 40 | [Fact] 41 | public async Task Test_get_test_records() 42 | { 43 | using var response = await test.Client.GetAsync("/api/get-test-records"); 44 | var content = await response.Content.ReadAsStringAsync(); 45 | 46 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 47 | response?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 48 | 49 | var expextedContent = """ 50 | [ 51 | ["1","t","one"], 52 | ["2","f","two"], 53 | ["3",null,"three"], 54 | [null,"t","foo,bar"], 55 | [null,null,"foo\"bar"], 56 | [null,null,null], 57 | ["1",null,"t"], 58 | ["1","t",null], 59 | [null,null,"\"foo\"bar\""], 60 | [null,null,"foo\"\"bar"], 61 | [null,null,"foo\"\"\"\"bar"], 62 | [null,null,"foo\"\",\"\"bar"], 63 | [null,null,"foo\\bar"], 64 | [null,null,"foo/bar"], 65 | [null,null,"foo\nbar"] 66 | ] 67 | """ 68 | .Replace(" ", "") 69 | .Replace("\r", "") 70 | .Replace("\n", ""); 71 | 72 | content.Should().Be(expextedContent); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /NpgsqlRestTests/SetofTableTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void SetofTableTests() 6 | { 7 | script.Append(@" 8 | create table test_table ( 9 | id int, 10 | name text not null 11 | ); 12 | 13 | insert into test_table values (1, 'one'), (2, 'two'), (3, 'three'); 14 | 15 | create function get_test_table() 16 | returns setof test_table 17 | language sql 18 | as 19 | $$ 20 | select * from test_table; 21 | $$; 22 | "); 23 | } 24 | } 25 | 26 | [Collection("TestFixture")] 27 | public class SetofTableTests(TestFixture test) 28 | { 29 | [Fact] 30 | public async Task Test_get_test_table() 31 | { 32 | using var response = await test.Client.GetAsync("/api/get-test-table"); 33 | var content = await response.Content.ReadAsStringAsync(); 34 | 35 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 36 | response?.Content?.Headers?.ContentType?.MediaType.Should().Be("application/json"); 37 | content.Should().Be("[{\"id\":1,\"name\":\"one\"},{\"id\":2,\"name\":\"two\"},{\"id\":3,\"name\":\"three\"}]"); 38 | } 39 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/Setup/Database.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Npgsql; 3 | 4 | namespace NpgsqlRestTests; 5 | 6 | public static partial class Database 7 | { 8 | private const string Dbname = "npgsql_rest_test"; 9 | private const string InitialConnectionString = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres"; 10 | private static readonly StringBuilder script = new(); 11 | 12 | static Database() 13 | { 14 | foreach (var method in typeof(Database).GetMethods(BindingFlags.Static | BindingFlags.Public)) 15 | { 16 | if (method.GetParameters().Length == 0 && !string.Equals(method.Name, "Create", StringComparison.OrdinalIgnoreCase)) 17 | { 18 | method.Invoke(null, []); 19 | } 20 | } 21 | } 22 | 23 | public static NpgsqlConnection CreateConnection() 24 | { 25 | var builder = new NpgsqlConnectionStringBuilder(InitialConnectionString) 26 | { 27 | Database = Dbname 28 | }; 29 | return new NpgsqlConnection(builder.ConnectionString); 30 | } 31 | 32 | public static string Create() 33 | { 34 | DropIfExists(); 35 | var builder = new NpgsqlConnectionStringBuilder(InitialConnectionString) 36 | { 37 | Database = Dbname 38 | }; 39 | 40 | using NpgsqlConnection test = new(builder.ConnectionString); 41 | test.Open(); 42 | using var command = test.CreateCommand(); 43 | command.CommandText = script.ToString(); 44 | command.ExecuteNonQuery(); 45 | 46 | return builder.ConnectionString; 47 | } 48 | 49 | public static string CreateAdditional(string appName) 50 | { 51 | var builder = new NpgsqlConnectionStringBuilder(InitialConnectionString) 52 | { 53 | Database = Dbname, 54 | ApplicationName = appName 55 | }; 56 | 57 | return builder.ConnectionString; 58 | } 59 | 60 | public static void DropIfExists() 61 | { 62 | using NpgsqlConnection connection = new(InitialConnectionString); 63 | connection.Open(); 64 | void Exec(string sql) 65 | { 66 | using var command = connection.CreateCommand(); 67 | command.CommandText = sql; 68 | command.ExecuteNonQuery(); 69 | } 70 | bool Any(string sql) 71 | { 72 | using var command = connection.CreateCommand(); 73 | command.CommandText = sql; 74 | using var reader = command.ExecuteReader(); 75 | if (reader.Read()) 76 | { 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | if (Any($"select 1 from pg_database where datname = '{Dbname}'")) 83 | { 84 | Exec($"revoke connect on database {Dbname} from public"); 85 | Exec($"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{Dbname}' and pid <> pg_backend_pid()"); 86 | Exec($"drop database {Dbname}"); 87 | } 88 | Exec($"create database {Dbname}"); 89 | Exec($"alter database {Dbname} set timezone to 'UTC'"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /NpgsqlRestTests/Setup/TestFixture.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | [CollectionDefinition("TestFixture")] 4 | public class TestFixtureCollection : ICollectionFixture { } 5 | 6 | public class TestFixture : IDisposable 7 | { 8 | private readonly WebApplicationFactory _application; 9 | private readonly HttpClient _client; 10 | 11 | public HttpClient Client => _client; 12 | public WebApplicationFactory Application => _application; 13 | 14 | public TestFixture() 15 | { 16 | _application = new WebApplicationFactory(); 17 | _client = _application.CreateClient(); 18 | _client.Timeout = TimeSpan.FromHours(1); 19 | } 20 | 21 | #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize 22 | public void Dispose() 23 | #pragma warning restore CA1816 // Dispose methods should call SuppressFinalize 24 | { 25 | _client.Dispose(); 26 | _application.Dispose(); 27 | } 28 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/Setup/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Microsoft.AspNetCore.Mvc.Testing; 3 | global using NpgsqlRest; 4 | global using System.Text; 5 | global using FluentAssertions; 6 | global using System.Net; 7 | global using System.Text.Json.Nodes; 8 | global using Microsoft.AspNetCore.Http.Extensions; -------------------------------------------------------------------------------- /NpgsqlRestTests/SingleRecordTests.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 2 | namespace NpgsqlRestTests; 3 | 4 | public static partial class Database 5 | { 6 | public static void SingleRecordTests() 7 | { 8 | script.Append( 9 | """ 10 | create table customers ( 11 | customer_id bigint not null PRIMARY KEY, 12 | name text NOT NULL, 13 | email text NULL, 14 | created_at TIMESTAMP NOT NULL default '2024-02-29' 15 | ); 16 | 17 | comment on table customers is 'disabled'; 18 | 19 | insert into customers 20 | (customer_id, name, email) 21 | values 22 | (1, 'test', 'email@email.com'); 23 | 24 | 25 | create function get_latest_customer() returns customers language sql 26 | as $$ 27 | select * 28 | from customers 29 | order by created_at 30 | limit 1 31 | $$; 32 | 33 | create function get_latest_customer_record() returns record language sql 34 | as $$ 35 | select * 36 | from customers 37 | order by created_at 38 | limit 1 39 | $$; 40 | 41 | """); 42 | } 43 | } 44 | 45 | [Collection("TestFixture")] 46 | public class SingleRecordTests(TestFixture test) 47 | { 48 | [Fact] 49 | public async Task Test_get_latest_customer() 50 | { 51 | using var response = await test.Client.GetAsync($"/api/get-latest-customer/"); 52 | var content = await response.Content.ReadAsStringAsync(); 53 | 54 | response.StatusCode.Should().Be(HttpStatusCode.OK); 55 | response.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); 56 | content.Should().Be("{\"customerId\":1,\"name\":\"test\",\"email\":\"email@email.com\",\"createdAt\":\"2024-02-29T00:00:00\"}"); 57 | } 58 | 59 | [Fact] 60 | public async Task Test_get_latest_customer_record() 61 | { 62 | using var response = await test.Client.GetAsync($"/api/get-latest-customer-record/"); 63 | var content = await response.Content.ReadAsStringAsync(); 64 | 65 | response.StatusCode.Should().Be(HttpStatusCode.OK); 66 | response.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); 67 | content.Should().Be("[\"1\",\"test\",\"email@email.com\",\"2024-02-29 00:00:00\"]"); 68 | } 69 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/StoredProcedureTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void StoredProcedureTests() 6 | { 7 | script.Append(@" 8 | create table proc_test_tbl(i int, t text); 9 | insert into proc_test_tbl values (1, 'X'); 10 | 11 | create procedure proc_test(_t text) 12 | language plpgsql 13 | as 14 | $$ 15 | begin 16 | update proc_test_tbl set t = _t where i = 1; 17 | end; 18 | $$; 19 | 20 | create function get_proc_test_tbl() returns text language sql as 'select t from proc_test_tbl where i = 1'; 21 | "); 22 | } 23 | } 24 | 25 | [Collection("TestFixture")] 26 | public class StoredProcedureTests(TestFixture test) 27 | { 28 | [Fact] 29 | public async Task Test_proc_test() 30 | { 31 | using var body = new StringContent("{\"t\": \"YYY\"}", Encoding.UTF8); 32 | using var response = await test.Client.PostAsync("/api/proc-test/", body); 33 | response?.StatusCode.Should().Be(HttpStatusCode.NoContent); 34 | 35 | using var getResponse = await test.Client.GetAsync("/api/get-proc-test-tbl/"); 36 | var getContent = await getResponse.Content.ReadAsStringAsync(); 37 | 38 | getResponse?.StatusCode.Should().Be(HttpStatusCode.OK); 39 | getContent.Should().Be("YYY"); 40 | } 41 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/StrictFunctionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void StrictFunctionsTests() 6 | { 7 | script.Append(@" 8 | create function strict_function(_p1 int, _p2 int) 9 | returns text 10 | strict 11 | language plpgsql 12 | as 13 | $$ 14 | begin 15 | raise info '_p1 = %, _p2 = %', _p1, _p2; 16 | return 'strict'; 17 | end; 18 | $$; 19 | 20 | create function returns_null_on_null_input_function(_p1 int, _p2 int) 21 | returns text 22 | returns null on null input 23 | language plpgsql 24 | as 25 | $$ 26 | begin 27 | raise info '_p1 = %, _p2 = %', _p1, _p2; 28 | return 'returns null on null input'; 29 | end; 30 | $$; 31 | "); 32 | } 33 | } 34 | 35 | [Collection("TestFixture")] 36 | public class StrictFunctionsTests(TestFixture test) 37 | { 38 | [Fact] 39 | public async Task Test_strict_function_NoNulls() 40 | { 41 | using var body = new StringContent("{\"p1\": 1, \"p2\": 2}", Encoding.UTF8, "application/json"); 42 | using var response = await test.Client.PostAsync("/api/strict-function", body); 43 | var content = await response.Content.ReadAsStringAsync(); 44 | 45 | response.StatusCode.Should().Be(HttpStatusCode.OK); 46 | response.Content.Headers.ContentType?.MediaType.Should().Be("text/plain"); 47 | 48 | content.Should().Be("strict"); 49 | } 50 | 51 | [Fact] 52 | public async Task Test_strict_function_OneNull() 53 | { 54 | using var body = new StringContent("{\"p1\": 1, \"p2\": null}", Encoding.UTF8, "application/json"); 55 | using var response = await test.Client.PostAsync("/api/strict-function", body); 56 | var content = await response.Content.ReadAsStringAsync(); 57 | 58 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 59 | content.Should().Be(""); 60 | } 61 | 62 | [Fact] 63 | public async Task Test_strict_function_TwoNulls() 64 | { 65 | using var body = new StringContent("{\"p1\": null, \"p2\": null}", Encoding.UTF8, "application/json"); 66 | using var response = await test.Client.PostAsync("/api/strict-function", body); 67 | var content = await response.Content.ReadAsStringAsync(); 68 | 69 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 70 | content.Should().Be(""); 71 | } 72 | 73 | 74 | [Fact] 75 | public async Task Test_returns_null_on_null_input_function_NoNulls() 76 | { 77 | using var body = new StringContent("{\"p1\": 1, \"p2\": 2}", Encoding.UTF8, "application/json"); 78 | using var response = await test.Client.PostAsync("/api/returns-null-on-null-input-function", body); 79 | var content = await response.Content.ReadAsStringAsync(); 80 | 81 | response.StatusCode.Should().Be(HttpStatusCode.OK); 82 | response.Content.Headers.ContentType?.MediaType.Should().Be("text/plain"); 83 | 84 | content.Should().Be("returns null on null input"); 85 | } 86 | 87 | [Fact] 88 | public async Task Test_returns_null_on_null_input_function_OneNull() 89 | { 90 | using var body = new StringContent("{\"p1\": 1, \"p2\": null}", Encoding.UTF8, "application/json"); 91 | using var response = await test.Client.PostAsync("/api/returns-null-on-null-input-function", body); 92 | var content = await response.Content.ReadAsStringAsync(); 93 | 94 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 95 | content.Should().Be(""); 96 | } 97 | 98 | [Fact] 99 | public async Task Test_returns_null_on_null_input_function_TwoNulls() 100 | { 101 | using var body = new StringContent("{\"p1\": null, \"p2\": null}", Encoding.UTF8, "application/json"); 102 | using var response = await test.Client.PostAsync("/api/returns-null-on-null-input-function", body); 103 | var content = await response.Content.ReadAsStringAsync(); 104 | 105 | response.StatusCode.Should().Be(HttpStatusCode.NoContent); 106 | content.Should().Be(""); 107 | } 108 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/TimestampTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void TimestampTests() 6 | { 7 | script.Append(@" 8 | create function get_timestamp() returns timestamp language sql as 9 | $$ 10 | select '2021-01-31 12:34:56.789'::timestamp; 11 | $$; 12 | 13 | create function get_timestamp_array() returns timestamp[] language sql as 14 | $$ 15 | select array['2021-01-30 12:34:56.789'::timestamp, '2021-01-31 12:34:56.789'::timestamp]; 16 | $$; 17 | 18 | create function get_timestamp_setof() returns setof timestamp language sql as 19 | $$ 20 | select '2021-01-30 12:34:56.789'::timestamp 21 | union 22 | select '2021-01-31 12:34:56.789'::timestamp; 23 | $$; 24 | 25 | create function get_timestamp_table() returns table (ts timestamp) language sql as 26 | $$ 27 | select '2021-01-30 12:34:56.789'::timestamp 28 | union 29 | select '2021-01-31 12:34:56.789'::timestamp; 30 | $$; 31 | "); 32 | } 33 | } 34 | 35 | [Collection("TestFixture")] 36 | public class TimestampTests(TestFixture test) 37 | { 38 | [Fact] 39 | public async Task Test_get_timestamp() 40 | { 41 | using var response = await test.Client.GetAsync("/api/get-timestamp"); 42 | var content = await response.Content.ReadAsStringAsync(); 43 | 44 | response.StatusCode.Should().Be(HttpStatusCode.OK); 45 | response.Content.Headers.ContentType?.MediaType.Should().Be("text/plain"); 46 | 47 | content.Should().Be("2021-01-31 12:34:56.789"); 48 | } 49 | 50 | [Fact] 51 | public async Task Test_get_timestamp_array() 52 | { 53 | using var response = await test.Client.GetAsync("/api/get-timestamp-array"); 54 | var content = await response.Content.ReadAsStringAsync(); 55 | 56 | response.StatusCode.Should().Be(HttpStatusCode.OK); 57 | response.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); 58 | 59 | content.Should().Be("[\"2021-01-30T12:34:56.789\",\"2021-01-31T12:34:56.789\"]"); 60 | } 61 | 62 | [Fact] 63 | public async Task Test_get_timestamp_setof() 64 | { 65 | using var response = await test.Client.GetAsync("/api/get-timestamp-setof"); 66 | var content = await response.Content.ReadAsStringAsync(); 67 | 68 | response.StatusCode.Should().Be(HttpStatusCode.OK); 69 | response.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); 70 | 71 | content.Should().Be("[\"2021-01-30T12:34:56.789\",\"2021-01-31T12:34:56.789\"]"); 72 | } 73 | 74 | [Fact] 75 | public async Task Test_get_timestamp_table() 76 | { 77 | using var response = await test.Client.GetAsync("/api/get-timestamp-table"); 78 | var content = await response.Content.ReadAsStringAsync(); 79 | 80 | response.StatusCode.Should().Be(HttpStatusCode.OK); 81 | response.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); 82 | 83 | content.Should().Be("[{\"ts\":\"2021-01-30T12:34:56.789\"},{\"ts\":\"2021-01-31T12:34:56.789\"}]"); 84 | } 85 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/UploadTests/CsvExampleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text.Json; 3 | using Npgsql; 4 | 5 | namespace NpgsqlRestTests; 6 | 7 | public static partial class Database 8 | { 9 | public static void CsvExampleTests() 10 | { 11 | script.Append(@" 12 | -- table for uploads 13 | create table csv_example_upload_table ( 14 | index int, 15 | id int, 16 | name text, 17 | value int 18 | ); 19 | 20 | -- row command 21 | create procedure csv_example_upload_table_row( 22 | _index int, 23 | _row text[] 24 | ) 25 | language plpgsql 26 | as 27 | $$ 28 | begin 29 | insert into csv_example_upload_table ( 30 | index, 31 | id, 32 | name, 33 | value 34 | ) 35 | values ( 36 | _index, 37 | _row[1]::int, 38 | _row[2], 39 | _row[3]::int 40 | ); 41 | end; 42 | $$; 43 | 44 | -- HTTP POST endpoint 45 | create function csv_example_upload( 46 | _meta json = null 47 | ) 48 | returns json 49 | language plpgsql 50 | as 51 | $$ 52 | begin 53 | -- do something with metadata or raise exception to rollback this upload 54 | return _meta; 55 | end; 56 | $$; 57 | 58 | comment on function csv_example_upload(json) is ' 59 | HTTP POST 60 | upload for csv 61 | param _meta is upload metadata 62 | delimiters = ,; 63 | row_command = call csv_example_upload_table_row($1,$2) 64 | '; 65 | "); 66 | } 67 | } 68 | 69 | [Collection("TestFixture")] 70 | public class CsvExampleTests(TestFixture test) 71 | { 72 | [Fact] 73 | public async Task Test_csv_mixed_delimiter_upload() 74 | { 75 | var fileName = "test-csv-upload.csv"; 76 | var sb = new StringBuilder(); 77 | sb.AppendLine("11,XXX,333"); 78 | sb.AppendLine("12;YYY;666"); 79 | sb.AppendLine("13;;999"); 80 | sb.AppendLine("14,,,"); 81 | var csvContent = sb.ToString(); 82 | var contentBytes = Encoding.UTF8.GetBytes(csvContent); 83 | using var formData = new MultipartFormDataContent(); 84 | using var byteContent = new ByteArrayContent(contentBytes); 85 | byteContent.Headers.ContentType = new MediaTypeHeaderValue("text/csv"); 86 | formData.Add(byteContent, "file", fileName); 87 | 88 | using var result = await test.Client.PostAsync("/api/csv-example-upload/", formData); 89 | result.StatusCode.Should().Be(HttpStatusCode.OK); 90 | } 91 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/VoidUnitTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void VoidUnitTests() 6 | { 7 | script.Append(@" 8 | create function case_void() 9 | returns void 10 | language plpgsql 11 | as 12 | $$ 13 | begin 14 | raise info 'case_void'; 15 | end; 16 | $$; 17 | "); 18 | } 19 | } 20 | 21 | [Collection("TestFixture")] 22 | public class VoidUnitTests(TestFixture test) 23 | { 24 | [Fact] 25 | public async Task Test_case_void() 26 | { 27 | using var result = await test.Client.PostAsync("/api/case-void/", null); 28 | result.StatusCode.Should().Be(HttpStatusCode.NoContent); 29 | } 30 | } -------------------------------------------------------------------------------- /NpgsqlRestTests/VolatilityVerbTests.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRestTests; 2 | 3 | public static partial class Database 4 | { 5 | public static void VolatilityVerbTests() 6 | { 7 | script.Append(@" 8 | create function volatile_func() 9 | returns text 10 | language sql 11 | volatile 12 | as $$ 13 | select 'volatile' 14 | $$; 15 | 16 | create function stable_func() 17 | returns text 18 | language sql 19 | stable 20 | as $$ 21 | select 'stable' 22 | $$; 23 | 24 | create function immutable_func() 25 | returns text 26 | language sql 27 | immutable 28 | as $$ 29 | select 'immutable' 30 | $$; 31 | "); 32 | } 33 | } 34 | 35 | [Collection("TestFixture")] 36 | public class VolatilityVerbTests(TestFixture test) 37 | { 38 | [Fact] 39 | public async Task Test_volatile_func() 40 | { 41 | using var response = await test.Client.PostAsync("/api/volatile-func/", null); 42 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 43 | } 44 | 45 | [Fact] 46 | public async Task Test_stable_func_Post() 47 | { 48 | using var response = await test.Client.PostAsync("/api/stable-func/", null); 49 | response?.StatusCode.Should().Be(HttpStatusCode.NotFound); 50 | } 51 | 52 | [Fact] 53 | public async Task Test_stable_func_Get() 54 | { 55 | using var response = await test.Client.GetAsync("/api/stable-func/"); 56 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 57 | } 58 | 59 | [Fact] 60 | public async Task Test_immutable_func_Post() 61 | { 62 | using var response = await test.Client.PostAsync("/api/immutable-func/", null); 63 | response?.StatusCode.Should().Be(HttpStatusCode.NotFound); 64 | } 65 | 66 | [Fact] 67 | public async Task Test_immutable_func_Get() 68 | { 69 | using var response = await test.Client.GetAsync("/api/immutable-func/"); 70 | response?.StatusCode.Should().Be(HttpStatusCode.OK); 71 | } 72 | } -------------------------------------------------------------------------------- /PerfomanceTests/k6/script.js: -------------------------------------------------------------------------------- 1 | import { check } from "k6"; 2 | import http from "k6/http"; 3 | import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; 4 | 5 | const now = new Date(); 6 | const stamp = '2_16__0__' + now.getFullYear() + 7 | String(now.getMonth() + 1).padStart(2, '0') + 8 | String(now.getDate()).padStart(2, '0') + 9 | String(now.getHours()).padStart(2, '0') + 10 | String(now.getMinutes()).padStart(2, '0'); //__ENV.STAMP; 11 | const tag = "localhost" //__ENV.TAG; 12 | const records = 50; //Number(__ENV.RECORDS || "10") 13 | const duration = "60s"; //__ENV.DURATION || "60s"; 14 | const target = 100; //Number(__ENV.TARGET || "100"); 15 | 16 | const url = 'http://localhost:5000/api/test-data' + "?" + 17 | Object.entries({ 18 | _records: records, 19 | _text_param: 'ABCDEFGHIJKLMNOPRSTUVWXYZ', 20 | _int_param: 1234567890, 21 | _ts_param: new Date('2014-12-31').toISOString(), 22 | _bool_param: true 23 | }) 24 | .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) 25 | .join('&'); 26 | 27 | // define configuration 28 | export const options = { 29 | // define thresholds 30 | thresholds: { 31 | http_req_failed: [{ threshold: "rate<0.01", abortOnFail: true }], // availability threshold for error rate 32 | http_req_duration: ["p(99)<1000"], // Latency threshold for percentile 33 | }, 34 | // define scenarios 35 | scenarios: { 36 | breaking: { 37 | executor: "ramping-vus", 38 | stages: [ 39 | { duration: duration, target: target }, 40 | ], 41 | }, 42 | }, 43 | }; 44 | 45 | export default function () { 46 | const res = http.get(url); 47 | check(res, { 48 | [`${tag} status is 200`]: (r) => r.status === 200, 49 | [`${tag} response is JSON`]: (r) => r.headers['Content-Type'] && r.headers['Content-Type'].includes('application/json'), 50 | [`${tag} response has data`]: (r) => r.body && JSON.parse(r.body).length > 0, 51 | }); 52 | } 53 | 54 | export function handleSummary(data) { 55 | return { 56 | [`${stamp}_${tag}_summary.txt`]: textSummary(data, { indent: ' ', enableColors: false }), 57 | [`${stamp}_${tag}.csv`]: `${data.metrics.http_reqs.values.count},${data.metrics.http_reqs.values.rate},${data.metrics.http_req_failed.values.rate}`, 58 | //[`/results/${stamp}/${tag}_summary.json`]: JSON.stringify(data, null, 2), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /PerfomanceTests/readme.md: -------------------------------------------------------------------------------- 1 | # Performance Tests 2 | 3 | Perfomance tests are moved to another repository: https://github.com/vb-consulting/pg_function_load_tests 4 | 5 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:25.04 AS builder 2 | WORKDIR /app 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends wget ca-certificates && \ 5 | wget https://github.com/vb-consulting/NpgsqlRest/releases/download/v2.27.0-client-v2.22.0/npgsqlrest-linux64 -O npgsqlrest && \ 6 | chmod +x npgsqlrest 7 | 8 | FROM ubuntu:25.04 9 | WORKDIR /app 10 | COPY --from=builder /app/npgsqlrest /usr/local/bin/npgsqlrest 11 | 12 | RUN apt-get update && \ 13 | apt-get install -y --no-install-recommends libssl3 ca-certificates && \ 14 | update-ca-certificates && \ 15 | rm -rf /var/lib/apt/lists/* && \ 16 | apt-get clean autoclean && \ 17 | apt-get autoremove --yes && \ 18 | rm -rf /var/lib/{apt,dpkg,cache,log}/ 19 | 20 | ENV PATH="/usr/local/bin:${PATH}" 21 | 22 | ENTRYPOINT ["npgsqlrest"] -------------------------------------------------------------------------------- /docker/readme.md: -------------------------------------------------------------------------------- 1 | # NpgsqlRect Docker 2 | 3 | To use the NpgsqlRect Docker, pull it from the registry: 4 | 5 | ```console 6 | $ docker pull vbilopav/npgsqlrest 7 | ``` 8 | 9 | # Features 10 | 11 | - Create an Automatic REST API for the PostgreSQL Databases. 12 | - Generate TypeScript Code and HTTP files for testing. 13 | - Configure security for use the of either encrypted cookies or JWT Bearer tokens or both. 14 | - Expose REST API endpoints for the PostgreSQL Databases as Login/Logout. 15 | - Use external authentication providers such as Google, LinkedIn or GitHub. 16 | - Server static content. 17 | - Use and configure built-in Serilog structured logging. 18 | - Configure Cross-origin resource sharing (CORS) access, SSL, Server Certificates and more, everything needed for modern Web development. 19 | 20 | See the [default configuration file](https://vb-consulting.github.io/npgsqlrest/config/) with descriptions for more information. 21 | 22 | # Running Containers 23 | 24 | ```console 25 | $ docker run --rm -i -t vbilopav/npgsqlrest --version 26 | ``` 27 | 28 | Outputs: 29 | 30 | ``` 31 | Versions: 32 | .NET 9.0.5 33 | Client Build 2.22.0.0 34 | Serilog.AspNetCore 9.0.0.0 35 | Npgsql 9.0.3.0 36 | NpgsqlRest 2.27.0.0 37 | NpgsqlRest.HttpFiles 1.3.0.0 38 | NpgsqlRest.TsClient 1.19.0.0 39 | NpgsqlRest.CrudSource 1.3.2.0 40 | 41 | CurrentDirectory /app 42 | 43 | ``` 44 | 45 | ```console 46 | $ docker run --rm -i -t vbilopav/npgsqlrest --help 47 | ``` 48 | 49 | Outputs: 50 | 51 | ``` 52 | Usage: 53 | npgsqlrest Run with the optional default configuration files: appsettings.json and 54 | appsettings.Development.json. If these file are not found, default 55 | configuration setting is used (see 56 | https://vb-consulting.github.io/npgsqlrest/config/). 57 | npgsqlrest [files...] Run with the custom configuration files. All configuration files are required. 58 | Any configuration values will override default values in order of appearance. 59 | npgsqlrest [file1 -o file2...] Use the -o switch to mark the next configuration file as optional. The first 60 | file after the -o switch is optional. 61 | npgsqlrest [file1 --optional file2...] Use --optional switch to mark the next configuration file as optional. The 62 | first file after the --optional switch is optional. 63 | Note: Values in the later file will override the values in the previous one. 64 | 65 | npgsqlrest [--key=value] Override the configuration with this key with a new value (case insensitive, 66 | use : to separate sections). 67 | 68 | npgsqlrest -v, --version Show version information. 69 | npgsqlrest -h, --help Show command line help. 70 | 71 | 72 | Examples: 73 | Example: use two config files npgsqlrest appsettings.json appsettings.Development.json 74 | Example: second config file optional npgsqlrest appsettings.json -o appsettings.Development.json 75 | Example: override ApplicationName config npgsqlrest --applicationname=Test 76 | Example: override Auth:CookieName config npgsqlrest --auth:cookiename=Test 77 | 78 | ``` 79 | 80 | Running in the current directory with two configuration files and exposing port 5000: 81 | 82 | ``` 83 | docker run --rm -i -t -v $(pwd):/home --expose 5000 vbilopav/npgsqlrest /home/appsettings.json /home/appsettings.Development.json 84 | ``` 85 | 86 | Note: To learn more about exposing ports and binding volumes, see the Docker documentation. To see more details on the NpgslRest configuration, see the [default configuration file](https://vb-consulting.github.io/npgsqlrest/config/). 87 | 88 | -------------------------------------------------------------------------------- /npm/config-copy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | // Get the destination directory from the command line arguments, or use the current directory 7 | const destDir = process.argv[2] || process.cwd(); 8 | 9 | // Path to the source file 10 | const srcFile = path.join(__dirname, "appsettings.json"); 11 | 12 | // Path to the destination file 13 | const destFile = path.join(destDir, "appsettings.json"); 14 | 15 | // Copy the file 16 | fs.copyFileSync(srcFile, destFile); 17 | 18 | console.log(`Copied appsettings.json to ${destFile}`); -------------------------------------------------------------------------------- /npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npgsqlrest", 3 | "version": "2.22.0", 4 | "description": "Automatic REST API for PostgreSQL Databases Client Build", 5 | "scripts": { 6 | "postinstall": "node postinstall.js", 7 | "uninstall": "node uninstall.js", 8 | "start": "npx npgsqlrest ./appsettings.json ./npgsqlrest.json" 9 | }, 10 | "bin": { 11 | "npgsqlrest": "node_modules/.bin/npgsqlrest", 12 | "npgsqlrest-config-copy": "./config-copy.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/vb-consulting/NpgsqlRest.git" 17 | }, 18 | "keywords": [ 19 | "postgresql", 20 | "server", 21 | "rest", 22 | "api", 23 | "automatic" 24 | ], 25 | "author": "vb-consulting", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/vb-consulting/NpgsqlRest/issues" 29 | }, 30 | "homepage": "https://github.com/vb-consulting/NpgsqlRest/blob/master/npm/readme.md" 31 | } 32 | -------------------------------------------------------------------------------- /npm/postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const os = require("os"); 6 | const https = require("https"); 7 | 8 | const downloadDir = "../.bin/"; 9 | const downloadFrom = "https://github.com/vb-consulting/NpgsqlRest/releases/download/v2.27.0-client-v2.22.0/"; 10 | 11 | function download(url, to, done) { 12 | https.get(url, (response) => { 13 | if (response.statusCode == 200) { 14 | const file = fs.createWriteStream(to, { mode: 0o755 }); 15 | response.pipe(file); 16 | file.on("finish", () => { 17 | file.close(); 18 | console.info(`${to} ...`,); 19 | if (done) { 20 | done(); 21 | } 22 | }); 23 | } else if (response.statusCode == 302) { 24 | download(response.headers.location, to); 25 | } else { 26 | console.error("Error downloading file:", to, response.statusCode, response.statusMessage); 27 | } 28 | }).on("error", (err) => { 29 | fs.unlink(to, () => { 30 | console.error("Error downloading file:", to, err); 31 | }); 32 | }); 33 | } 34 | 35 | const osType = os.type(); 36 | var downloadFileUrl; 37 | var downloadTo; 38 | 39 | if (osType === "Windows_NT") { 40 | downloadFileUrl = `${downloadFrom}npgsqlrest-win64.exe`; 41 | downloadTo = `${downloadDir}npgsqlrest.exe`; 42 | } else if (osType === "Linux") { 43 | downloadFileUrl = `${downloadFrom}npgsqlrest-linux64`; 44 | downloadTo = `${downloadDir}npgsqlrest`; 45 | } else if (osType === "Darwin") { 46 | downloadFileUrl = `${downloadFrom}npgsqlrest-osx64`; 47 | downloadTo = `${downloadDir}npgsqlrest`; 48 | } else { 49 | console.error("Unsupported OS detected:", osType); 50 | process.exit(1); 51 | } 52 | 53 | if (!fs.existsSync(path.dirname(downloadTo))) { 54 | fs.mkdirSync(path.dirname(downloadTo), { recursive: true }); 55 | } 56 | 57 | if (fs.existsSync(downloadTo)) { 58 | fs.unlinkSync(downloadTo); 59 | } 60 | download(downloadFileUrl, downloadTo); 61 | 62 | 63 | downloadFileUrl = `${downloadFrom}appsettings.json`; 64 | downloadTo = "./appsettings.json"; 65 | if (fs.existsSync(downloadFileUrl)) { 66 | fs.unlinkSync(downloadFileUrl, downloadTo); 67 | } 68 | download(downloadFileUrl, downloadTo); 69 | -------------------------------------------------------------------------------- /npm/uninstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const os = require("os"); 5 | 6 | const downloadDir = "../.bin/"; 7 | const osType = os.type(); 8 | 9 | var downloadTo; 10 | 11 | if (osType === "Windows_NT") { 12 | downloadTo = `${downloadDir}npgsqlrest.exe`; 13 | } else { 14 | downloadTo = `${downloadDir}npgsqlrest`; 15 | } 16 | 17 | if (fs.existsSync(downloadTo)) { 18 | fs.unlinkSync(downloadTo); 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /plugins/NpgsqlRest.CrudSource/CrudSourceQuery.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.CrudSource; 2 | 3 | internal class CrudSourceQuery 4 | { 5 | public const string Query = """ 6 | with _cte1 as ( 7 | select 8 | nspname as schema 9 | from 10 | pg_catalog.pg_namespace 11 | where 12 | nspname not like 'pg_%' 13 | and nspname <> 'information_schema' 14 | and ($1 is null or nspname similar to $1) 15 | and ($2 is null or nspname not similar to $2) 16 | and ($3 is null or nspname = any($3)) 17 | and ($4 is null or not nspname = any($4)) 18 | ), _cte2 as ( 19 | select 20 | tc.table_name, 21 | tc.table_schema, 22 | coalesce(array_agg(kcu.column_name), '{}'::text[]) as primary_keys 23 | from information_schema.table_constraints as tc 24 | join information_schema.key_column_usage as kcu on tc.constraint_name = kcu.constraint_name and tc.table_schema = kcu.table_schema 25 | join _cte1 on tc.table_schema = _cte1.schema 26 | where 27 | tc.constraint_type = 'PRIMARY KEY' 28 | and ($5 is null or tc.table_name similar to $5) 29 | and ($6 is null or tc.table_name not similar to $6) 30 | and ($7 is null or tc.table_name = any($7)) 31 | and ($8 is null or not tc.table_name = any($8)) 32 | group by 33 | tc.table_name, 34 | tc.table_schema 35 | ) 36 | select 37 | t.table_type as type, 38 | quote_ident(t.table_schema) as schema, 39 | quote_ident(t.table_name) as name, 40 | t.is_insertable_into = 'YES' as is_insertable, 41 | 42 | count(*)::int as column_count, 43 | coalesce( 44 | array_agg(c.column_name order by c.ordinal_position), 45 | '{}'::text[] 46 | ) as column_names, 47 | 48 | coalesce( 49 | array_agg( 50 | case when c.data_type = 'bit' then 'varbit' else (c.udt_schema || '.' || c.udt_name)::regtype::text end 51 | order by c.ordinal_position 52 | ), 53 | '{}'::text[] 54 | ) as column_types, 55 | 56 | coalesce( 57 | array_agg(c.is_updatable = 'YES' order by c.ordinal_position), 58 | '{}'::boolean[] 59 | ) as updatable_columns, 60 | 61 | coalesce( 62 | array_agg(coalesce(c.identity_generation, '') = 'ALWAYS' order by c.ordinal_position), 63 | '{}'::boolean[] 64 | ) as identity_columns, 65 | 66 | coalesce( 67 | array_agg( 68 | ( 69 | c.is_nullable = 'YES' 70 | or c.column_default is not null 71 | or c.is_identity = 'YES' 72 | or c.is_generated <> 'NEVER' 73 | )::boolean 74 | order by c.ordinal_position 75 | ), 76 | '{}'::boolean[] 77 | ) as has_defaults, 78 | 79 | coalesce(_cte2.primary_keys, '{}'::text[]) as primary_keys, 80 | pgdesc.description as comment 81 | from 82 | information_schema.columns c 83 | join _cte1 on c.table_schema = _cte1.schema 84 | join information_schema.tables t on c.table_schema = t.table_schema and c.table_name = t.table_name 85 | left join _cte2 on c.table_schema = _cte2.table_schema and c.table_name = _cte2.table_name 86 | left join pg_catalog.pg_stat_all_tables pgtbl 87 | on t.table_name = pgtbl.relname and t.table_schema = pgtbl.schemaname 88 | left join pg_catalog.pg_description pgdesc 89 | on pgtbl.relid = pgdesc.objoid and pgdesc.objsubid = 0 90 | where 91 | (t.table_type = 'VIEW' or t.table_type = 'BASE TABLE') 92 | and ($5 is null or t.table_name similar to $5) 93 | and ($6 is null or t.table_name not similar to $6) 94 | and ($7 is null or t.table_name = any($7)) 95 | and ($8 is null or not t.table_name = any($8)) 96 | group by 97 | t.table_type, 98 | t.table_schema, 99 | t.table_name, 100 | t.is_insertable_into, 101 | _cte2.primary_keys, 102 | pgdesc.description 103 | """; 104 | } 105 | -------------------------------------------------------------------------------- /plugins/NpgsqlRest.CrudSource/NpgsqlRest.CrudSource.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net9.0 6 | enable 7 | enable 8 | true 9 | $(NoWarn);1591 10 | true 11 | NpgsqlRest.CrudSource 12 | VB-Consulting 13 | VB-Consulting 14 | VB-Consulting 15 | CRUD Source for NpgsqlRest 16 | api;api-rest;restful-api;http;postgres;dotnet;crud;database;rest;server;postgresql;npgsqlrest;pgsql;pg;automatic 17 | https://github.com/vb-consulting/NpgsqlRest/NpgsqlRest.CrudSource 18 | https://github.com/vb-consulting/NpgsqlRest 19 | https://github.com/vb-consulting/NpgsqlRest/blob/master/changelog.md 20 | NpgsqlRest.CrudSource 21 | LICENSE 22 | true 23 | true 24 | snupkg 25 | true 26 | true 27 | true 28 | true 29 | README.MD 30 | bin\$(Configuration)\$(AssemblyName).xml 31 | 1.3.2 32 | 1.3.2 33 | 1.3.2 34 | 1.3.2 35 | 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | True 49 | 50 | 51 | 52 | 53 | 54 | 55 | True 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /plugins/NpgsqlRest.HttpFiles/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.HttpFiles; 2 | 3 | public enum HttpFileOption { Disabled, File, Endpoint, Both } 4 | public enum HttpFileMode { Database, Schema } 5 | -------------------------------------------------------------------------------- /plugins/NpgsqlRest.HttpFiles/HttpFileOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NpgsqlRest.HttpFiles; 2 | 3 | public class HttpFileOptions( 4 | HttpFileOption option = HttpFileOption.Both, 5 | string namePattern = "{0}_{1}", 6 | CommentHeader commentHeader = CommentHeader.Simple, 7 | bool commentHeaderIncludeComments = true, 8 | HttpFileMode fileMode = HttpFileMode.Database, 9 | bool fileOverwrite = false, 10 | string? connectionString = null, 11 | string? name = null) 12 | { 13 | public static HttpFileOptions CreateBoth() => new(option: HttpFileOption.Both); 14 | public static HttpFileOptions CreateFile() => new(option: HttpFileOption.File); 15 | public static HttpFileOptions CreateEndpoint() => new(option: HttpFileOption.Endpoint); 16 | 17 | public HttpFileOptions() : this(option: HttpFileOption.Both) { } 18 | 19 | /// 20 | /// Options for HTTP file generation: 21 | /// Disabled - skip. 22 | /// File - creates a file on disk. 23 | /// Endpoint - exposes file content as endpoint. 24 | /// Both - creates a file on disk and exposes file content as endpoint. 25 | /// 26 | public HttpFileOption Option { get; set; } = option; 27 | /// 28 | /// The pattern to use when generating file names. {0} is database name, {1} is schema suffix with underline when FileMode is set to Schema. 29 | /// Use this property to set the custom file name. 30 | /// .http extension will be added automatically. 31 | /// 32 | public string NamePattern { get; set; } = namePattern; 33 | /// 34 | /// Adds comment header to above request based on PostgreSQL routine 35 | /// Set None to skip. 36 | /// Set Simple (default) to add name, parameters and return values to comment header. 37 | /// Set Full to add the entire routine code as comment header. 38 | /// 39 | public CommentHeader CommentHeader { get; set; } = commentHeader; 40 | /// 41 | /// When CommentHeader is set to Simple or Full, set to true to include routine comments in comment header. 42 | /// 43 | public bool CommentHeaderIncludeComments { get; set; } = commentHeaderIncludeComments; 44 | /// 45 | /// Set to Database to create one http file for entire database. 46 | /// Set to Schema to create new http file for every database schema. 47 | /// 48 | public HttpFileMode FileMode { get; set; } = fileMode; 49 | /// 50 | /// Set to true to overwrite existing files. 51 | /// 52 | public bool FileOverwrite { get; set; } = fileOverwrite; 53 | /// 54 | /// The connection string to the database used in NpgsqlRest. 55 | /// Used to get the name of the database for the file name. 56 | /// If Name property is set, this property is ignored. 57 | /// 58 | public string? ConnectionString { get; set; } = connectionString; 59 | /// 60 | /// File name. If not set, the database name will be used if connection string is set. 61 | /// If neither ConnectionString nor Name is set, the file name will be "npgsqlrest". 62 | /// 63 | public string? Name { get; set; } = name; 64 | } 65 | -------------------------------------------------------------------------------- /plugins/NpgsqlRest.HttpFiles/NpgsqlRest.HttpFiles.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net9.0 6 | enable 7 | enable 8 | true 9 | $(NoWarn);1591 10 | true 11 | NpgsqlRest.HttpFiles 12 | VB-Consulting 13 | VB-Consulting 14 | VB-Consulting 15 | Automatic HTTP Files Generation for NpgsqlRest 16 | api;api-rest;restful-api;http;postgres;dotnet;http;httpfiles;http-files;database;rest;server;postgresql;npgsqlrest;pgsql;pg;automatic 17 | https://github.com/vb-consulting/NpgsqlRest/NpgsqlRest.HttpFiles 18 | https://github.com/vb-consulting/NpgsqlRest 19 | https://github.com/vb-consulting/NpgsqlRest/blob/master/changelog.md 20 | NpgsqlRest.HttpFiles 21 | LICENSE 22 | true 23 | true 24 | snupkg 25 | true 26 | true 27 | true 28 | true 29 | README.MD 30 | bin\$(Configuration)\$(AssemblyName).xml 31 | 1.3.0 32 | 1.3.0 33 | 1.3.0 34 | 1.3.0 35 | 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | True 49 | 50 | 51 | 52 | 53 | 54 | 55 | True 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /plugins/NpgsqlRest.TsClient/NpgsqlRest.TsClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net9.0 6 | enable 7 | enable 8 | true 9 | $(NoWarn);1591 10 | true 11 | NpgsqlRest.TsClient 12 | VB-Consulting 13 | VB-Consulting 14 | VB-Consulting 15 | Automatic Typescript Client Code Generation for NpgsqlRest 16 | api;api-rest;restful-api;postgres;dotnet;ts;Typescript;code-gen;database;rest;server;postgresql;npgsqlrest;pgsql;pg;automatic 17 | https://github.com/vb-consulting/NpgsqlRest/NpgsqlRest.TsClient 18 | https://github.com/vb-consulting/TsClient 19 | https://github.com/vb-consulting/NpgsqlRest/blob/master/changelog.md 20 | NpgsqlRest.TsClient 21 | LICENSE 22 | true 23 | true 24 | snupkg 25 | true 26 | true 27 | true 28 | true 29 | README.MD 30 | bin\$(Configuration)\$(AssemblyName).xml 31 | 1.19.0 32 | 1.19.0 33 | 1.19.0 34 | 1.19.0 35 | 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | True 49 | 50 | 51 | 52 | 53 | 54 | 55 | True 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | --------------------------------------------------------------------------------