├── .documentation ├── .gitignore ├── docfx.json ├── index.md └── toc.yml ├── .github ├── icon.png ├── logo.png ├── postgrest-relationship-example.drawio ├── postgrest-relationship-example.drawio.png └── workflows │ ├── build-and-test.yml │ ├── build-documentation.yaml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Postgrest ├── Attributes │ ├── ColumnAttribute.cs │ ├── PrimaryKeyAttribute.cs │ ├── ReferenceAttribute.cs │ └── TableAttribute.cs ├── Client.cs ├── ClientOptions.cs ├── Constants.cs ├── Converters │ ├── DateTimeConverter.cs │ ├── IntConverter.cs │ └── RangeConverter.cs ├── Debugger.cs ├── Exceptions │ ├── FailureHint.cs │ └── PostgrestException.cs ├── Extensions │ ├── EnumExtensions.cs │ ├── RangeExtensions.cs │ ├── TypeExtensions.cs │ └── UriExtensions.cs ├── Helpers.cs ├── Hooks.cs ├── IntRange.cs ├── Interfaces │ ├── IPostgrestCacheProvider.cs │ ├── IPostgrestClient.cs │ ├── IPostgrestDebugger.cs │ ├── IPostgrestQueryFilter.cs │ ├── IPostgrestTable.cs │ └── IPostgrestTableWithCache.cs ├── Linq │ ├── SelectExpressionVisitor.cs │ ├── SetExpressionVisitor.cs │ └── WhereExpressionVisitor.cs ├── Models │ ├── BaseModel.cs │ └── CachedModel.cs ├── Postgrest.csproj ├── PostgrestContractResolver.cs ├── QueryFilter.cs ├── QueryOptions.cs ├── QueryOrderer.cs ├── Requests │ └── CacheBackedRequest.cs ├── Responses │ ├── BaseResponse.cs │ └── ModeledResponse.cs ├── Table.cs └── TableWithCache.cs ├── PostgrestExample ├── Models │ ├── Message.cs │ ├── Movie.cs │ └── User.cs ├── PostgrestExample.csproj └── Program.cs ├── PostgrestTests ├── ClientTests.cs ├── CoercionTests.cs ├── ConverterTests.cs ├── ExceptionTests.cs ├── ExtensionTests.cs ├── Helpers.cs ├── LinqTests.cs ├── Models │ ├── ForeignKeyTestModel.cs │ ├── KitchenSink.cs │ ├── LinkedModels.cs │ ├── Message.cs │ ├── NestedForeignKeyTestModel.cs │ ├── Stub.cs │ ├── Todo.cs │ └── User.cs ├── PostgrestTests.csproj ├── ReferenceTests.cs ├── TableWithCacheTests.cs └── db │ ├── 00-schema.sql │ └── 01-dummy-data.sql ├── README.md ├── Supabase.Postgrest.sln └── docker-compose.yml /.documentation/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | /_site 10 | /api 11 | -------------------------------------------------------------------------------- /.documentation/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "../Postgrest", 7 | "files": [ 8 | "**/*.csproj" 9 | ] 10 | } 11 | ], 12 | "dest": "api" 13 | } 14 | ], 15 | "build": { 16 | "content": [ 17 | { 18 | "files": [ 19 | "**/*.{md,yml}" 20 | ], 21 | "exclude": [ 22 | "_site/**" 23 | ] 24 | } 25 | ], 26 | "resource": [ 27 | { 28 | "files": [ 29 | "images/**" 30 | ] 31 | } 32 | ], 33 | "output": "_site", 34 | "template": [ 35 | "default", 36 | "modern" 37 | ], 38 | "globalMetadata": { 39 | "_appName": "postgrest-csharp", 40 | "_appTitle": "postgrest-csharp", 41 | "_enableSearch": true 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /.documentation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | _layout: landing 3 | --- 4 | 5 | # postgrest-csharp 6 | 7 | ![Build And Test](https://github.com/supabase/postgrest-csharp/workflows/Build%20And%20Test/badge.svg) 8 | [![Nuget Release](https://img.shields.io/badge/dynamic/json?color=green&label=Nuget%20Release&query=data[0].version&url=https%3A%2F%2Fazuresearch-usnc.nuget.org%2Fquery%3Fq%3Dpackageid%3Apostgrest-csharp)](https://www.nuget.org/packages/postgrest-csharp/) 9 | 10 | Postgrest-csharp is written primarily as a helper library for [supabase/supabase-csharp](https://github.com/supabase/supabase-csharp), however, it should be easy enough to use outside of the supabase ecosystem. 11 | 12 | The bulk of this library is a translation and c-sharp-ification of the [supabase/postgrest-js](https://github.com/supabase/postgrest-js) library. 13 | -------------------------------------------------------------------------------- /.documentation/toc.yml: -------------------------------------------------------------------------------- 1 | - name: API 2 | href: api/ 3 | -------------------------------------------------------------------------------- /.github/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/postgrest-csharp/879920a16211b41e97bf2c0dc96aba8aa21e1f85/.github/icon.png -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/postgrest-csharp/879920a16211b41e97bf2c0dc96aba8aa21e1f85/.github/logo.png -------------------------------------------------------------------------------- /.github/postgrest-relationship-example.drawio: -------------------------------------------------------------------------------- 1 | 7Vldb9owFP01SN1DUb7I6GP5ah82TWonbX1CJrkkXh07cxxK9utnxw5JSKmoNChDvLTxude+1/c4xxfoueNkfcdRGn9lIZCeY4XrnjvpOY7vePKvAgoN3Fg3Gog4DjVk18Aj/gMGtAya4xCylqNgjAictsGAUQqBaGGIc/bSdlsy0o6aogg6wGOASBf9gUMRa3To+DV+DziKq8i2b/a3QMFzxFlOTTzKKGhLgqplzB6zGIXspQG505475owJ/ZSsx0BUVauK6XmzHdZNyhyo2GcCG0/49+CJezd3eTF7uCuiVXFt25/1OitEclMM2u/3qclZFFWJIJQVM0PGRcwiRhGZ1uioLAOoaJYc1T5fGEslaEvwFwhRGPpRLpiEYpEQY80E4uJW0SmB6UOCaFGhM0yIWRho2PGRWMND564S3lkqA2Us54Hx2hw8xCMQb9bM29AnXwhgCQheyJkcCBJ41Y6KzNGMNn41R/LB0PQuyoYdymxJmf0RlMmC8uLnhhk5eFKD/qAaTtZN46Qwoy7V4TcKe7Bt3I5M+OBIhFdrNKjtOT6RyY3SFrf+71zpxiiRuWPac2+l1UrXZS0sDV4LxaEyeJVBwFpcI4IjMyOQ5QHesIUQMC63xIyDOh2c4LLkdVD5FJn/ZWqZ4IxGFZrmC4KDfgo8k8sYF7nxtlfDkG5jMd9G3r95AkuhLUNj2c5e1vyNHA4RcYl5JuYUJXDsyAR9UOCAAxIQzpF4PfKWYq2ACyyv5Vt9RiflER6ZEzvR4UZMei1JKQnLUgNGS0aFkSjbMeMZSjBRcnMPZAVq1e3LhrNnGDPCeBnbtSzX9X1lqS5qu+SMkIbTxJvas4EJ0Z28S3bUvmDdgLpaYqye6RZMI+VW3cNL3ZbYvsHiZktiWQe7b/xLi2DkeXByLUKV0hncGAlb4aZAXS6Mi2L/f4ote3Hf20+zh8O+6x9Mtb3zEob5paHsRNSFKeOiRL13dJGl2t4ac1iCPOEBZGp1NekKh5+Ona0m8JV0W+lpr4/Ir1p70TktR6sQx9JXacszFFeqUOOKsHk12lTx0775nqNid+T5FRHfqdj2oKXY9qDbY2++mWz12PbheuzzaeNSziShp9zImQzLV8o6EfGBBGFy6fVOXDmcmz0+ng//0cdzOax/HShtjR9f3Olf -------------------------------------------------------------------------------- /.github/postgrest-relationship-example.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/postgrest-csharp/879920a16211b41e97bf2c0dc96aba8aa21e1f85/.github/postgrest-relationship-example.drawio.png -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v3 17 | with: 18 | dotnet-version: 8.x 19 | 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | 23 | - name: Build 24 | run: dotnet build --configuration Release --no-restore 25 | 26 | - name: Initialize Testing Stack 27 | run: docker compose up -d 28 | 29 | - name: Test 30 | run: dotnet test --no-restore 31 | -------------------------------------------------------------------------------- /.github/workflows/build-documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* # Default release branch 8 | 9 | jobs: 10 | docs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v3 19 | with: 20 | dotnet-version: 8.x 21 | 22 | - name: Install docfx 23 | run: dotnet tool update -g docfx 24 | 25 | - name: Build documentation 26 | run: docfx .documentation/docfx.json 27 | 28 | - name: Deploy 🚀 29 | uses: JamesIves/github-pages-deploy-action@v4 30 | with: 31 | folder: .documentation/_site 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish NuGet Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - release/* # Default release branch 7 | 8 | jobs: 9 | publish: 10 | name: build, pack & publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v3 17 | with: 18 | dotnet-version: 8.x 19 | 20 | - name: Wait for tests to succeed 21 | uses: lewagon/wait-on-check-action@v1.3.1 22 | with: 23 | ref: ${{ github.ref }} 24 | check-name: build-and-test 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} 26 | wait-interval: 10 27 | 28 | - name: Restore dependencies 29 | run: dotnet restore 30 | 31 | - name: Build 32 | run: dotnet build ./Postgrest/Postgrest.csproj --configuration Release --no-restore 33 | 34 | - name: Generate package 35 | run: dotnet pack ./Postgrest/Postgrest.csproj --configuration Release 36 | 37 | - name: Publish on version change 38 | run: dotnet nuget push "**/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # globs 2 | Makefile.in 3 | *.userprefs 4 | *.usertasks 5 | config.make 6 | config.status 7 | aclocal.m4 8 | install-sh 9 | autom4te.cache/ 10 | *.tar.gz 11 | tarballs/ 12 | test-results/ 13 | 14 | # Mac bundle stuff 15 | *.dmg 16 | *.app 17 | 18 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 19 | # General 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 48 | # Windows thumbnail cache files 49 | Thumbs.db 50 | ehthumbs.db 51 | ehthumbs_vista.db 52 | 53 | # Dump file 54 | *.stackdump 55 | 56 | # Folder config file 57 | [Dd]esktop.ini 58 | 59 | # Recycle Bin used on file shares 60 | $RECYCLE.BIN/ 61 | 62 | # Windows Installer files 63 | *.cab 64 | *.msi 65 | *.msix 66 | *.msm 67 | *.msp 68 | 69 | # Windows shortcuts 70 | *.lnk 71 | 72 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 73 | ## Ignore Visual Studio temporary files, build results, and 74 | ## files generated by popular Visual Studio add-ons. 75 | ## 76 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 77 | 78 | # User-specific files 79 | *.suo 80 | *.user 81 | *.userosscache 82 | *.sln.docstates 83 | 84 | # User-specific files (MonoDevelop/Xamarin Studio) 85 | *.userprefs 86 | 87 | # Build results 88 | [Dd]ebug/ 89 | [Dd]ebugPublic/ 90 | [Rr]elease/ 91 | [Rr]eleases/ 92 | x64/ 93 | x86/ 94 | bld/ 95 | [Bb]in/ 96 | [Oo]bj/ 97 | [Ll]og/ 98 | 99 | # Visual Studio 2015/2017 cache/options directory 100 | .vs/ 101 | # Uncomment if you have tasks that create the project's static files in wwwroot 102 | #wwwroot/ 103 | 104 | # Visual Studio 2017 auto generated files 105 | Generated\ Files/ 106 | 107 | # MSTest test Results 108 | [Tt]est[Rr]esult*/ 109 | [Bb]uild[Ll]og.* 110 | 111 | # NUNIT 112 | *.VisualState.xml 113 | TestResult.xml 114 | 115 | # Build Results of an ATL Project 116 | [Dd]ebugPS/ 117 | [Rr]eleasePS/ 118 | dlldata.c 119 | 120 | # Benchmark Results 121 | BenchmarkDotNet.Artifacts/ 122 | 123 | # .NET Core 124 | project.lock.json 125 | project.fragment.lock.json 126 | artifacts/ 127 | 128 | # StyleCop 129 | StyleCopReport.xml 130 | 131 | # Files built by Visual Studio 132 | *_i.c 133 | *_p.c 134 | *_h.h 135 | *.ilk 136 | *.meta 137 | *.obj 138 | *.iobj 139 | *.pch 140 | *.pdb 141 | *.ipdb 142 | *.pgc 143 | *.pgd 144 | *.rsp 145 | *.sbr 146 | *.tlb 147 | *.tli 148 | *.tlh 149 | *.tmp 150 | *.tmp_proj 151 | *_wpftmp.csproj 152 | *.log 153 | *.vspscc 154 | *.vssscc 155 | .builds 156 | *.pidb 157 | *.svclog 158 | *.scc 159 | 160 | # Chutzpah Test files 161 | _Chutzpah* 162 | 163 | # Visual C++ cache files 164 | ipch/ 165 | *.aps 166 | *.ncb 167 | *.opendb 168 | *.opensdf 169 | *.sdf 170 | *.cachefile 171 | *.VC.db 172 | *.VC.VC.opendb 173 | 174 | # Visual Studio profiler 175 | *.psess 176 | *.vsp 177 | *.vspx 178 | *.sap 179 | 180 | # Visual Studio Trace Files 181 | *.e2e 182 | 183 | # TFS 2012 Local Workspace 184 | $tf/ 185 | 186 | # Guidance Automation Toolkit 187 | *.gpState 188 | 189 | # ReSharper is a .NET coding add-in 190 | _ReSharper*/ 191 | *.[Rr]e[Ss]harper 192 | *.DotSettings.user 193 | 194 | # JustCode is a .NET coding add-in 195 | .JustCode 196 | 197 | # TeamCity is a build add-in 198 | _TeamCity* 199 | 200 | # DotCover is a Code Coverage Tool 201 | *.dotCover 202 | 203 | # AxoCover is a Code Coverage Tool 204 | .axoCover/* 205 | !.axoCover/settings.json 206 | 207 | # Visual Studio code coverage results 208 | *.coverage 209 | *.coveragexml 210 | 211 | # NCrunch 212 | _NCrunch_* 213 | .*crunch*.local.xml 214 | nCrunchTemp_* 215 | 216 | # MightyMoose 217 | *.mm.* 218 | AutoTest.Net/ 219 | 220 | # Web workbench (sass) 221 | .sass-cache/ 222 | 223 | # Installshield output folder 224 | [Ee]xpress/ 225 | 226 | # DocProject is a documentation generator add-in 227 | DocProject/buildhelp/ 228 | DocProject/Help/*.HxT 229 | DocProject/Help/*.HxC 230 | DocProject/Help/*.hhc 231 | DocProject/Help/*.hhk 232 | DocProject/Help/*.hhp 233 | DocProject/Help/Html2 234 | DocProject/Help/html 235 | 236 | # Click-Once directory 237 | publish/ 238 | 239 | # Publish Web Output 240 | *.[Pp]ublish.xml 241 | *.azurePubxml 242 | # Note: Comment the next line if you want to checkin your web deploy settings, 243 | # but database connection strings (with potential passwords) will be unencrypted 244 | *.pubxml 245 | *.publishproj 246 | 247 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 248 | # checkin your Azure Web App publish settings, but sensitive information contained 249 | # in these scripts will be unencrypted 250 | PublishScripts/ 251 | 252 | # NuGet Packages 253 | *.nupkg 254 | # The packages folder can be ignored because of Package Restore 255 | **/[Pp]ackages/* 256 | # except build/, which is used as an MSBuild target. 257 | !**/[Pp]ackages/build/ 258 | # Uncomment if necessary however generally it will be regenerated when needed 259 | #!**/[Pp]ackages/repositories.config 260 | # NuGet v3's project.json files produces more ignorable files 261 | *.nuget.props 262 | *.nuget.targets 263 | 264 | # Microsoft Azure Build Output 265 | csx/ 266 | *.build.csdef 267 | 268 | # Microsoft Azure Emulator 269 | ecf/ 270 | rcf/ 271 | 272 | # Windows Store app package directories and files 273 | AppPackages/ 274 | BundleArtifacts/ 275 | Package.StoreAssociation.xml 276 | _pkginfo.txt 277 | *.appx 278 | 279 | # Visual Studio cache files 280 | # files ending in .cache can be ignored 281 | *.[Cc]ache 282 | # but keep track of directories ending in .cache 283 | !*.[Cc]ache/ 284 | 285 | # Others 286 | ClientBin/ 287 | ~$* 288 | *~ 289 | *.dbmdl 290 | *.dbproj.schemaview 291 | *.jfm 292 | *.pfx 293 | *.publishsettings 294 | orleans.codegen.cs 295 | 296 | # Including strong name files can present a security risk 297 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 298 | #*.snk 299 | 300 | # Since there are multiple workflows, uncomment next line to ignore bower_components 301 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 302 | #bower_components/ 303 | 304 | # RIA/Silverlight projects 305 | Generated_Code/ 306 | 307 | # Backup & report files from converting an old project file 308 | # to a newer Visual Studio version. Backup files are not needed, 309 | # because we have git ;-) 310 | _UpgradeReport_Files/ 311 | Backup*/ 312 | UpgradeLog*.XML 313 | UpgradeLog*.htm 314 | ServiceFabricBackup/ 315 | *.rptproj.bak 316 | 317 | # SQL Server files 318 | *.mdf 319 | *.ldf 320 | *.ndf 321 | 322 | # Business Intelligence projects 323 | *.rdl.data 324 | *.bim.layout 325 | *.bim_*.settings 326 | *.rptproj.rsuser 327 | 328 | # Microsoft Fakes 329 | FakesAssemblies/ 330 | 331 | # GhostDoc plugin setting file 332 | *.GhostDoc.xml 333 | 334 | # Node.js Tools for Visual Studio 335 | .ntvs_analysis.dat 336 | node_modules/ 337 | 338 | # Visual Studio 6 build log 339 | *.plg 340 | 341 | # Visual Studio 6 workspace options file 342 | *.opt 343 | 344 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 345 | *.vbw 346 | 347 | # Visual Studio LightSwitch build output 348 | **/*.HTMLClient/GeneratedArtifacts 349 | **/*.DesktopClient/GeneratedArtifacts 350 | **/*.DesktopClient/ModelManifest.xml 351 | **/*.Server/GeneratedArtifacts 352 | **/*.Server/ModelManifest.xml 353 | _Pvt_Extensions 354 | 355 | # Paket dependency manager 356 | .paket/paket.exe 357 | paket-files/ 358 | 359 | # FAKE - F# Make 360 | .fake/ 361 | 362 | # JetBrains Rider 363 | .idea/ 364 | *.sln.iml 365 | 366 | # CodeRush personal settings 367 | .cr/personal 368 | 369 | # Python Tools for Visual Studio (PTVS) 370 | __pycache__/ 371 | *.pyc 372 | 373 | # Cake - Uncomment if you are using it 374 | # tools/** 375 | # !tools/packages.config 376 | 377 | # Tabs Studio 378 | *.tss 379 | 380 | # Telerik's JustMock configuration file 381 | *.jmconfig 382 | 383 | # BizTalk build output 384 | *.btp.cs 385 | *.btm.cs 386 | *.odx.cs 387 | *.xsd.cs 388 | 389 | # OpenCover UI analysis results 390 | OpenCover/ 391 | 392 | # Azure Stream Analytics local run output 393 | ASALocalRun/ 394 | 395 | # MSBuild Binary and Structured Log 396 | *.binlog 397 | 398 | # NVidia Nsight GPU debugger configuration file 399 | *.nvuser 400 | 401 | # MFractors (Xamarin productivity tool) working folder 402 | .mfractor/ 403 | 404 | # Local History for Visual Studio 405 | .localhistory/ 406 | /PostgrestTests/.runsettings 407 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.1.0 - 2025-02-03 4 | 5 | - Added count to ModeledResponse [#103](https://github.com/supabase-community/postgrest-csharp/pull/103) by [DanielW093]https://github.com/DanielW093 6 | - Add support for long, DateTime, and DateTimeOffset criteria in filter expressions [#101](https://github.com/supabase-community/postgrest-csharp/pull/101) by [sbarnes-ellenbytech](https://github.com/sbarnes-ellenbytech) 7 | 8 | ## 4.0.3 - 2024-05-23 9 | 10 | - Re: [#97](https://github.com/supabase-community/postgrest-csharp/pull/97) Fix set null value on string property. 11 | Thanks [@alustrement-bob](https://github.com/alustrement-bob)! 12 | 13 | ## 4.0.2 - 2024-05-16 14 | 15 | - Re: [#96](https://github.com/supabase-community/postgrest-csharp/pull/96) Set `ConfigureAwait(false)` the response to 16 | prevent deadlocking applications. Thanks [@pur3extreme](https://github.com/pur3extreme)! 17 | 18 | ## 4.0.1 - 2024-05-07 19 | 20 | - Re: [#92](https://github.com/supabase-community/postgrest-csharp/issues/92) Changes `IPostgrestTable<>` contract to 21 | return the interface rather than a concrete type. 22 | 23 | ## 4.0.0 - 2024-04-21 24 | 25 | - [MAJOR] Moves namespaces from `Postgrest` to `Supabase.Postgrest` 26 | - Re: [#135](https://github.com/supabase-community/supabase-csharp/issues/135) Update nuget package 27 | name `postgrest-csharp` to `Supabase.Postgrest` 28 | 29 | ## 3.5.1 - 2024-03-15 30 | 31 | - Re: [#147](https://github.com/supabase-community/supabase-csharp/issues/147) - Supports `Rpc` specifying a generic 32 | type for its return. 33 | 34 | ## 3.5.0 - 2024-01-14 35 | 36 | - Re: [#78](https://github.com/supabase-community/postgrest-csharp/issues/78), Generalize query filtering creation 37 | in `Table` so that it matches new generic signatures. 38 | - Move from `QueryFilter` parameters to a more generic `IPostgrestQueryFilter` to support constructing new QueryFilters 39 | from a LINQ expression. 40 | - Note: Lists of `QueryFilter`s will now need to be defined 41 | as: `new List { new QueryFilter(), ... }` 42 | - Adjust serialization of timestamps within a `QueryFilter` to support `DateTime` and `DateTimeOffset` using the 43 | ISO-8601 (https://stackoverflow.com/a/115002) 44 | 45 | ## 3.4.1 - 2024-01-08 46 | 47 | - Re: [#85](https://github.com/supabase-community/postgrest-csharp/issues/85) Fixes problem when using multiple .Order() 48 | methods by merging [#86](https://github.com/supabase-community/postgrest-csharp/pull/86). 49 | Thanks [@hunsra](https://github.com/hunsra)! 50 | 51 | ## 3.4.0 - 2024-01-03 52 | 53 | - Re: [#81](https://github.com/supabase-community/postgrest-csharp/issues/81) 54 | - [Minor] Removes `IgnoreOnInsert`and `IgnoreOnUpdate` from `ReferenceAttribute` as changing these properties 55 | to `false` does not currently provide the expected functionality. 56 | - Fixes `Insert` and `Update` not working on models that have `Reference` specified on a property with a non-null 57 | value. 58 | 59 | ## 3.3.0 - 2023-11-28 60 | 61 | - Re: [#78](https://github.com/supabase-community/postgrest-csharp/issues/78) Updates signatures for `Not` and `Filter` 62 | to include generic types for a better development experience. 63 | - Updates internal generic type names to be more descriptive. 64 | - Add support for LINQ predicates on `Table.Not()` signatures 65 | 66 | ## 3.2.10 - 2023-11-13 67 | 68 | - Re: [#76](https://github.com/supabase-community/postgrest-csharp/issues/76) Removes the incorrect `ToUniversalTime` 69 | conversion in the LINQ `Where` parser. 70 | 71 | ## 3.2.9 - 2023-10-09 72 | 73 | - Re: [supabase-csharp#115](https://github.com/supabase-community/supabase-csharp/discussions/115) Additional support 74 | for a model referencing another model with multiple foreign keys. 75 | 76 | ## 3.2.8 - 2023-10-08 77 | 78 | - Re: [supabase-csharp#115](https://github.com/supabase-community/supabase-csharp/discussions/115) Adds support for 79 | multiple references attached to the same model (foreign keys) on a single C# Model. 80 | 81 | ## 3.2.7 - 2023-09-15 82 | 83 | - Implements a `TableWithCache` for `Get` requests that can pull reactive Models from cache before making a remote 84 | request. 85 | - Re: [supabase-csharp#85](https://github.com/supabase-community/supabase-csharp/issues/85) Includes sourcelink support. 86 | 87 | ## 3.2.6 - 2023-09-04 88 | 89 | - Re: [#75](https://github.com/supabase-community/postgrest-csharp/pull/75) Fix issue with marshalling of stored 90 | procedure arguments. Big thank you to [@corrideat](https://github.com/corrideat)! 91 | 92 | ## 3.2.5 - 2023-07-13 93 | 94 | - Re: [supabase-community/supabase-csharp#81](https://github.com/supabase-community/supabase-csharp/discussions/81) - 95 | Clarifies `ReferenceAttribute` by changing `shouldFilterTopLevel` to `useInnerJoin` and adds an additional 96 | constructor for `ReferenceAttribute` with a shortcut for specifying the `JoinType` 97 | 98 | ## 3.2.4 - 2023-06-29 99 | 100 | - [#70](https://github.com/supabase-community/postgrest-csharp/pull/70) Minor Unity related fixes 101 | 102 | ## 3.2.3 - 2023-06-25 103 | 104 | - [#69](https://github.com/supabase-community/postgrest-csharp/pull/69) Locks language version to C#9 105 | - [#68](https://github.com/supabase-community/postgrest-csharp/pull/68) Makes RPC parameters optional 106 | 107 | Thanks [@wiverson](https://github.com/wiverson) for the work in this release! 108 | 109 | ## 3.2.2 - 2023-06-10 110 | 111 | - Uses new assembly name of `Supabase.Core` 112 | 113 | ## 3.2.1 - 2023-06-10 114 | 115 | - Changes Assembly output to be `Supabase.Postgrest` 116 | 117 | ## 3.2.0 - 2023-05-23 118 | 119 | - General codebase and QOL improvements. Exceptions are generally thrown through `PostgrestException` now instead 120 | of `Exception`. A `FailureHint.Reason` is provided with failures if possible to parse. 121 | - `AddDebugListener` is now available on the client to help with debugging 122 | - Merges [#65](https://github.com/supabase-community/postgrest-csharp/pull/65) Cleanup + Add better exception handling 123 | - Merges [#66](https://github.com/supabase-community/postgrest-csharp/pull/66) Local test Fixes 124 | - Fixes [#67](https://github.com/supabase-community/postgrest-csharp/issues/67) Postgrest Reference attribute is 125 | producing StackOverflow for circular references 126 | 127 | ## 3.1.3 - 2023-01-28 128 | 129 | - Fix [#61](https://github.com/supabase-community/postgrest-csharp/issues/61) which further typechecks nullable values. 130 | 131 | ## 3.1.2 - 2023-01-27 132 | 133 | - Fix [#61](https://github.com/supabase-community/postgrest-csharp/issues/61) which did not correctly parse Linq `Where` 134 | when encountering a nullable type. 135 | - Add missing support for transforming for `== null` and `!= null` 136 | 137 | ## 3.1.1 - 2023-01-17 138 | 139 | - Fix issue from supabase-community/supabase-csharp#48 where boolean model properties would not be evaluated in 140 | predicate expressions 141 | 142 | ## 3.1.0 - 2023-01-16 143 | 144 | - [Minor] Breaking API Change: `PrimaryKey` attribute defaults to `shouldInsert: false` as most uses will have the 145 | Database generate the primary key. 146 | - Merged [#60](https://github.com/supabase-community/postgrest-csharp/pull/60) which Added linq support 147 | for `Select`, `Where`, `OnConflict`, `Columns`, `Order`, `Update`, `Set`, and `Delete` 148 | 149 | ## 3.0.4 - 2022-11-22 150 | 151 | ## 3.0.3 - 2022-11-22 152 | 153 | - `GetHeaders` is now passed to `ModeledResponse` and `BaseModel` so that the default `Update` and `Delete` methods use 154 | the latest credentials 155 | - `GetHeaders` is used in `Rpc` calls (re: [#39](https://github.com/supabase-community/supabase-csharp/issues/39)) 156 | 157 | ## 3.0.2 - 2022-11-12 158 | 159 | - `IPostgrestClient` and `IPostgrestAPI` now implement `IGettableHeaders` 160 | 161 | ## 3.0.1 - 2022-11-10 162 | 163 | - Make `SerializerSettings` publicly accessible. 164 | 165 | ## 3.0.0 - 2022-11-08 166 | 167 | - Re: [#54](https://github.com/supabase-community/postgrest-csharp/pull/54) Restructure Project to support DI and enable 168 | Nullity 169 | - `Client` is no longer a singleton class. 170 | - `StatelessClient` has been removed as `Client` performs the same essential functions. 171 | - `Table` default constructor requires reference to `JsonSerializerSettings` 172 | - `BaseModel` now keeps track of `BaseUrl` and `RequestClientOptions`. These are now used in the default (and 173 | overridable) `BaseModel.Update` and `BaseModel.Delete` methods (as they previously referenced the singleton). 174 | - All publicly facing classes (that offer functionality) now include an Interface. 175 | - `RequestException` is no longer thrown for attempting to update a record that does not exist, instead an 176 | empty `ModeledResponse` is returned. 177 | 178 | ## 2.1.1 - 2022-10-19 179 | 180 | - 181 | 182 | Re: [#50](https://github.com/supabase-community/postgrest-csharp/issues/50) & [#51](https://github.com/supabase-community/postgrest-csharp/pull/51) 183 | Adds `shouldFilterTopRows` as constructor parameter for `ReferenceAttribute` which defaults to `true` to match current 184 | API expectations. 185 | 186 | ## 2.1.0 - 2022-10-11 187 | 188 | - [Minor] Breaking API change: Remove `BaseModel.PrimaryKeyValue` and `BaseModel.PrimaryKeyColumn` in favor of 189 | a `PrimaryKey` dictionary with support for composite keys. 190 | - Re: [#48](https://github.com/supabase-community/postgrest-csharp/issues/48) - Add support for derived models 191 | on `ReferenceAttribute` 192 | - Re: [#49](https://github.com/supabase-community/postgrest-csharp/issues/49) - Added `Match(T model)` 193 | 194 | ## 2.0.12 - 2022-09-13 195 | 196 | - Merged [#47](https://github.com/supabase-community/postgrest-csharp/pull/47) which added cancellation token support 197 | to `Table` methods. Thanks [@devpikachu](https://github.com/devpikachu)! 198 | 199 | ## 2.0.11 - 2022-08-01 200 | 201 | - Additional `OnConflict` Access via `QueryOptions` with reference 202 | to [supabase-community/supabase-csharp#29](https://github.com/supabase-community/supabase-csharp/issues/29) 203 | 204 | ## 2.0.10 - 2022-08-01 205 | 206 | - Added `OnConflict` parameter for UNIQUE resolution with reference 207 | to [supabase-community/supabase-csharp#29](https://github.com/supabase-community/supabase-csharp/issues/29) 208 | 209 | ## 2.0.9 - 2022-07-17 210 | 211 | - Merged [#44](https://github.com/supabase-community/postgrest-csharp/pull/44) Fixing zero length content when sending 212 | requests without body. Thanks [@SameerOmar](https://github.com/sameeromar)! 213 | 214 | ## 2.0.8 - 2022-05-24 215 | 216 | - Implements [#41](https://github.com/supabase-community/postgrest-csharp/issues/41), which adds support for `infinity` 217 | and `-infinity` as readable values. 218 | 219 | ## 2.0.7 - 2022-04-09 220 | 221 | - Merged [#39](https://github.com/supabase-community/postgrest-csharp/pull/39), which a fixed shadowed variable 222 | in `Table.And` and `Table.Or`. Thanks [@erichards3](https://github.com/erichards3)! 223 | 224 | ## 2.0.6 - 2021-12-30 225 | 226 | - Fix for [#38](https://github.com/supabase-community/postgrest-csharp/issues/38), Add support for `NullValueHandling` 227 | to be specified on a `Column` Attribute and for it to be honored on Inserts and Updates. Defaults 228 | to: `NullValueHandling.Include`. 229 | 230 | ## 2.0.5 - 2021-12-26 231 | 232 | - Fix for [#37](https://github.com/supabase-community/postgrest-csharp/issues/37) - Fixes #37 - Return Type `minimal` 233 | would fail to resolve because of incorrect `Accept` headers. Added header and test to verify for future. 234 | 235 | ## 2.0.4 - 2021-12-26 236 | 237 | - Fix for [#36](https://github.com/supabase-community/postgrest-csharp/issues/36) - Inserting/Upserting bulk records 238 | would fail while doing an unnecessary generic coercion. 239 | 240 | ## 2.0.3 - 2021-11-26 241 | 242 | - Add a `StatelessClient` static class (re: [#7](https://github.com/supabase-community/supabase-csharp/issues/7)) that 243 | enables API interactions through specifying `StatelessClientOptions` 244 | - Fix for [#35](https://github.com/supabase-community/postgrest-csharp/issues/35) - Client now handles DateTime[] 245 | serialization and deserialization. 246 | - Added tests for `StatelessClient` 247 | - Added "Kitchen Sink" tests for roundtrip serialization and deserialization data coersion. 248 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joseph Schultz 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 | -------------------------------------------------------------------------------- /Postgrest/Attributes/ColumnAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Newtonsoft.Json; 4 | namespace Supabase.Postgrest.Attributes 5 | { 6 | 7 | /// 8 | /// Used to map a C# property to a Postgrest Column. 9 | /// 10 | /// 11 | /// 12 | /// class User : BaseModel { 13 | /// [ColumnName("firstName")] 14 | /// public string FirstName {get; set;} 15 | /// } 16 | /// 17 | /// 18 | [AttributeUsage(AttributeTargets.Property)] 19 | public class ColumnAttribute : Attribute 20 | { 21 | /// 22 | /// The name in postgres of this column. 23 | /// 24 | public string ColumnName { get; } 25 | 26 | /// 27 | /// Specifies what should be serialized in the event this column's value is NULL 28 | /// 29 | public NullValueHandling NullValueHandling { get; set; } 30 | 31 | /// 32 | /// If the performed query is an Insert or Upsert, should this value be ignored? 33 | /// 34 | public bool IgnoreOnInsert { get; } 35 | 36 | /// 37 | /// If the performed query is an Update, should this value be ignored? 38 | /// 39 | public bool IgnoreOnUpdate { get; } 40 | 41 | /// 42 | public ColumnAttribute([CallerMemberName] string? columnName = null, NullValueHandling nullValueHandling = NullValueHandling.Include, bool ignoreOnInsert = false, bool ignoreOnUpdate = false) 43 | { 44 | ColumnName = columnName!; // Will either be user specified or given by runtime compiler. 45 | NullValueHandling = nullValueHandling; 46 | IgnoreOnInsert = ignoreOnInsert; 47 | IgnoreOnUpdate = ignoreOnUpdate; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Postgrest/Attributes/PrimaryKeyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | #pragma warning disable CS1591 4 | 5 | namespace Supabase.Postgrest.Attributes 6 | { 7 | 8 | /// 9 | /// Used to map a C# property to a Postgrest PrimaryKey. 10 | /// 11 | /// 12 | /// 13 | /// class User : BaseModel { 14 | /// [PrimaryKey("id")] 15 | /// public string Id {get; set;} 16 | /// } 17 | /// 18 | /// 19 | [AttributeUsage(AttributeTargets.Property)] 20 | public class PrimaryKeyAttribute : Attribute 21 | { 22 | public string ColumnName { get; } 23 | 24 | /// 25 | /// Would be set to false in the event that the database handles the generation of this property. 26 | /// 27 | public bool ShouldInsert { get; } 28 | 29 | public PrimaryKeyAttribute([CallerMemberName] string? columnName = null, bool shouldInsert = false) 30 | { 31 | ColumnName = columnName!; // Either given by user or specified by runtime compiler. 32 | ShouldInsert = shouldInsert; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Postgrest/Attributes/ReferenceAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using Supabase.Postgrest.Extensions; 6 | using Supabase.Postgrest.Exceptions; 7 | using Supabase.Postgrest.Models; 8 | 9 | namespace Supabase.Postgrest.Attributes 10 | { 11 | /// 12 | /// Used to specify that a foreign key relationship exists in PostgreSQL 13 | /// 14 | /// See: https://postgrest.org/en/stable/api.html#resource-embedding 15 | /// 16 | [AttributeUsage(AttributeTargets.Property)] 17 | public class ReferenceAttribute : Attribute 18 | { 19 | /// 20 | /// Specifies the Join type on this reference. PostgREST only allows for a LEFT join and an INNER join. 21 | /// 22 | public enum JoinType 23 | { 24 | /// 25 | /// INNER JOIN: returns rows when there is a match on both the source and the referenced tables. 26 | /// 27 | Inner, 28 | 29 | /// 30 | /// LEFT JOIN: returns all rows from the source table, even if there are no matches in the referenced table 31 | /// 32 | Left 33 | } 34 | 35 | /// 36 | /// Type of the model referenced 37 | /// 38 | public Type Model { get; } 39 | 40 | /// 41 | /// Column this attribute references as specified in Postgres, DOES NOT need to be set if is set. 42 | /// 43 | public string? ColumnName { get; private set; } 44 | 45 | /// 46 | /// The explicit SQL defined foreign key that this references. 47 | /// 48 | public string? ForeignKey { get; private set; } 49 | 50 | /// 51 | /// Table name of model 52 | /// 53 | public string TableName { get; } 54 | 55 | /// 56 | /// Columns that exist on the model we will select from. 57 | /// 58 | public List Columns { get; private set; } = new(); 59 | 60 | /// 61 | /// If the performed query is an Insert or Upsert, should this value be ignored? (DEFAULT TRUE) 62 | /// 63 | public bool IgnoreOnInsert { get; private set; } 64 | 65 | /// 66 | /// If the performed query is an Update, should this value be ignored? (DEFAULT TRUE) 67 | /// 68 | public bool IgnoreOnUpdate { get; private set; } 69 | 70 | /// 71 | /// If Reference should automatically be included in queries on this reference. (DEFAULT TRUE) 72 | /// 73 | public bool IncludeInQuery { get; } 74 | 75 | /// 76 | /// As to whether the query will filter top-level rows. 77 | /// 78 | /// See: https://postgrest.org/en/stable/api.html#resource-embedding 79 | /// 80 | public bool UseInnerJoin { get; } 81 | 82 | /// Establishes a reference between two tables 83 | /// Model referenced 84 | /// Should referenced be included in queries? 85 | /// Specifies the join type for this relationship 86 | /// Column this attribute references as specified in Postgres, DOES NOT need to be set if <see cref="ForeignKey"/> is set. 87 | /// Foreign Key this attribute references as specified in Postgres (only required if the model references the same table multiple times) 88 | /// 89 | public ReferenceAttribute(Type model, JoinType joinType, bool includeInQuery = true, 90 | [CallerMemberName] string columnName = "", string? foreignKey = null) 91 | : this(model, includeInQuery, joinType == JoinType.Inner, columnName, 92 | foreignKey) 93 | { 94 | } 95 | 96 | /// Establishes a reference between two tables 97 | /// Model referenced 98 | /// Should referenced be included in queries? 99 | /// As to whether the query will filter top-level rows. 100 | /// Column this attribute references as specified in Postgres, DOES NOT need to be set if is set. 101 | /// Foreign Key this attribute references as specified in Postgres (only required if the model references the same table multiple times) 102 | /// 103 | public ReferenceAttribute(Type model, bool includeInQuery = true, bool useInnerJoin = true, 104 | [CallerMemberName] string? columnName = null, string? foreignKey = null) 105 | { 106 | if (!IsDerivedFromBaseModel(model)) 107 | throw new PostgrestException("ReferenceAttribute must be used with Postgrest BaseModels.") 108 | { Reason = FailureHint.Reason.InvalidArgument }; 109 | 110 | Model = model; 111 | IncludeInQuery = includeInQuery; 112 | IgnoreOnInsert = true; 113 | IgnoreOnUpdate = true; 114 | ColumnName = columnName; 115 | UseInnerJoin = useInnerJoin; 116 | ForeignKey = foreignKey; 117 | 118 | var attr = GetCustomAttribute(model, typeof(TableAttribute)); 119 | TableName = attr is TableAttribute tableAttr ? tableAttr.Name : model.Name; 120 | } 121 | 122 | internal void ParseProperties(List? seenRefs = null) 123 | { 124 | seenRefs ??= new List(); 125 | 126 | ParseColumns(ref seenRefs); 127 | ParseRelationships(seenRefs); 128 | } 129 | 130 | private void ParseColumns(ref List seenRefs) 131 | { 132 | foreach (var property in Model.GetProperties()) 133 | { 134 | var attrs = property.GetCustomAttributes(true); 135 | 136 | foreach (var item in attrs) 137 | { 138 | switch (item) 139 | { 140 | case ColumnAttribute colAttr: 141 | Columns.Add(colAttr.ColumnName); 142 | break; 143 | case PrimaryKeyAttribute pkAttr: 144 | Columns.Add(pkAttr.ColumnName); 145 | break; 146 | } 147 | } 148 | } 149 | } 150 | 151 | /// 152 | public override bool Equals(object? obj) 153 | { 154 | if (obj is ReferenceAttribute attribute) 155 | { 156 | return TableName == attribute.TableName && ColumnName == attribute.ColumnName && 157 | Model == attribute.Model; 158 | } 159 | 160 | return false; 161 | } 162 | 163 | 164 | /// 165 | /// Parses relationships that exist on this model. Called by 166 | /// 167 | /// 168 | private void ParseRelationships(List seenRefs) 169 | { 170 | foreach (var property in Model.GetProperties()) 171 | { 172 | var attrs = property.GetCustomAttributes(true); 173 | 174 | foreach (var attr in attrs) 175 | { 176 | if (attr is not ReferenceAttribute { IncludeInQuery: true } refAttr) continue; 177 | 178 | if (seenRefs.FirstOrDefault(r => r.Equals(refAttr)) != null) continue; 179 | 180 | seenRefs.Add(refAttr); 181 | refAttr.ParseProperties(seenRefs); 182 | 183 | if (!string.IsNullOrEmpty(refAttr.ForeignKey)) 184 | { 185 | Columns.Add(UseInnerJoin 186 | ? $"{refAttr.ColumnName}:{refAttr.ForeignKey}!inner({string.Join(",", refAttr.Columns.ToArray())})" 187 | : $"{refAttr.ColumnName}:{refAttr.ForeignKey}({string.Join(",", refAttr.Columns.ToArray())})"); 188 | } 189 | else 190 | { 191 | Columns.Add(UseInnerJoin 192 | ? $"{refAttr.TableName}!inner({string.Join(",", refAttr.Columns.ToArray())})" 193 | : $"{refAttr.TableName}({string.Join(",", refAttr.Columns.ToArray())})"); 194 | } 195 | } 196 | } 197 | } 198 | 199 | private static bool IsDerivedFromBaseModel(Type type) => 200 | type.GetInheritanceHierarchy().Any(t => t == typeof(BaseModel)); 201 | } 202 | } -------------------------------------------------------------------------------- /Postgrest/Attributes/TableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | #pragma warning disable CS1591 3 | namespace Supabase.Postgrest.Attributes 4 | { 5 | 6 | /// 7 | /// Used to map a C# Model to a Postgres Table. 8 | /// 9 | /// 10 | /// 11 | /// [Table("user")] 12 | /// class User : BaseModel { 13 | /// [ColumnName("firstName")] 14 | /// public string FirstName {get; set;} 15 | /// } 16 | /// 17 | /// 18 | [AttributeUsage(AttributeTargets.Class)] 19 | public class TableAttribute : Attribute 20 | { 21 | public string Name { get; set; } 22 | 23 | public TableAttribute(string tableName) 24 | { 25 | Name = tableName; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Postgrest/Client.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Converters; 7 | using Supabase.Core.Extensions; 8 | using Supabase.Postgrest.Interfaces; 9 | using Supabase.Postgrest.Models; 10 | using Supabase.Postgrest.Responses; 11 | 12 | namespace Supabase.Postgrest 13 | { 14 | /// 15 | public class Client : IPostgrestClient 16 | { 17 | /// 18 | /// Custom Serializer resolvers and converters that will be used for encoding and decoding Postgrest JSON responses. 19 | /// 20 | /// By default, Postgrest seems to use a date format that C# and Newtonsoft do not like, so this initial 21 | /// configuration handles that. 22 | /// 23 | public static JsonSerializerSettings SerializerSettings(ClientOptions? options = null) 24 | { 25 | options ??= new ClientOptions(); 26 | 27 | return new JsonSerializerSettings 28 | { 29 | ContractResolver = new PostgrestContractResolver(), 30 | Converters = 31 | { 32 | // 2020-08-28T12:01:54.763231 33 | new IsoDateTimeConverter 34 | { 35 | DateTimeStyles = options.DateTimeStyles, 36 | DateTimeFormat = ClientOptions.DATE_TIME_FORMAT 37 | } 38 | } 39 | }; 40 | } 41 | 42 | /// 43 | public string BaseUrl { get; } 44 | 45 | /// 46 | public ClientOptions Options { get; } 47 | 48 | /// 49 | public void AddRequestPreparedHandler(OnRequestPreparedEventHandler handler) => 50 | Hooks.Instance.AddRequestPreparedHandler(handler); 51 | 52 | /// 53 | public void RemoveRequestPreparedHandler(OnRequestPreparedEventHandler handler) => 54 | Hooks.Instance.AddRequestPreparedHandler(handler); 55 | 56 | /// 57 | public void ClearRequestPreparedHandlers() => 58 | Hooks.Instance.ClearRequestPreparedHandlers(); 59 | 60 | /// 61 | public void AddDebugHandler(IPostgrestDebugger.DebugEventHandler handler) => 62 | Debugger.Instance.AddDebugHandler(handler); 63 | 64 | /// 65 | public void RemoveDebugHandler(IPostgrestDebugger.DebugEventHandler handler) => 66 | Debugger.Instance.RemoveDebugHandler(handler); 67 | 68 | /// 69 | public void ClearDebugHandlers() => Debugger.Instance.ClearDebugHandlers(); 70 | 71 | /// 72 | /// Function that can be set to return dynamic headers. 73 | /// 74 | /// Headers specified in the constructor options will ALWAYS take precedence over headers returned by this function. 75 | /// 76 | public Func>? GetHeaders { get; set; } 77 | 78 | /// 79 | /// Should be the first call to this class to initialize a connection with a Postgrest API Server 80 | /// 81 | /// Api Endpoint (ex: "http://localhost:8000"), no trailing slash required. 82 | /// Optional client configuration. 83 | /// 84 | public Client(string baseUrl, ClientOptions? options = null) 85 | { 86 | BaseUrl = baseUrl; 87 | Options = options ?? new ClientOptions(); 88 | } 89 | 90 | 91 | /// 92 | public IPostgrestTable Table() where T : BaseModel, new() => 93 | new Table(BaseUrl, SerializerSettings(Options), Options) 94 | { 95 | GetHeaders = GetHeaders 96 | }; 97 | 98 | /// 99 | public IPostgrestTableWithCache Table(IPostgrestCacheProvider cacheProvider) 100 | where T : BaseModel, new() => 101 | new TableWithCache(BaseUrl, cacheProvider, SerializerSettings(Options), Options) 102 | { 103 | GetHeaders = GetHeaders 104 | }; 105 | 106 | 107 | /// 108 | public async Task Rpc(string procedureName, object? parameters = null) 109 | { 110 | var response = await Rpc(procedureName, parameters); 111 | 112 | return string.IsNullOrEmpty(response.Content) ? default : JsonConvert.DeserializeObject(response.Content!); 113 | } 114 | 115 | /// 116 | public Task Rpc(string procedureName, object? parameters = null) 117 | { 118 | // Build Uri 119 | var builder = new UriBuilder($"{BaseUrl}/rpc/{procedureName}"); 120 | 121 | var canonicalUri = builder.Uri.ToString(); 122 | 123 | var serializerSettings = SerializerSettings(Options); 124 | 125 | // Prepare parameters 126 | Dictionary? data = null; 127 | if (parameters != null) 128 | data = JsonConvert.DeserializeObject>( 129 | JsonConvert.SerializeObject(parameters, serializerSettings)); 130 | 131 | // Prepare headers 132 | var headers = Helpers.PrepareRequestHeaders(HttpMethod.Post, 133 | new Dictionary(Options.Headers), Options); 134 | 135 | if (GetHeaders != null) 136 | headers = GetHeaders().MergeLeft(headers); 137 | 138 | // Send request 139 | var request = 140 | Helpers.MakeRequest(Options, HttpMethod.Post, canonicalUri, serializerSettings, data, headers); 141 | return request; 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /Postgrest/ClientOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | #pragma warning disable CS1591 4 | namespace Supabase.Postgrest 5 | { 6 | 7 | /// 8 | /// Options that can be passed to the Client configuration 9 | /// 10 | public class ClientOptions 11 | { 12 | public string Schema { get; set; } = "public"; 13 | 14 | public readonly DateTimeStyles DateTimeStyles = DateTimeStyles.AdjustToUniversal; 15 | 16 | public const string DATE_TIME_FORMAT = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFK"; 17 | 18 | public Dictionary Headers { get; set; } = new Dictionary(); 19 | 20 | public Dictionary QueryParams { get; set; } = new Dictionary(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Postgrest/Constants.cs: -------------------------------------------------------------------------------- 1 | using Supabase.Core.Attributes; 2 | #pragma warning disable CS1591 3 | namespace Supabase.Postgrest 4 | { 5 | 6 | public static class Constants 7 | { 8 | /// 9 | /// See: https://postgrest.org/en/v7.0.0/api.html?highlight=operators#operators 10 | /// 11 | public enum Operator 12 | { 13 | [MapTo("and")] 14 | And, 15 | [MapTo("or")] 16 | Or, 17 | [MapTo("eq")] 18 | Equals, 19 | [MapTo("gt")] 20 | GreaterThan, 21 | [MapTo("gte")] 22 | GreaterThanOrEqual, 23 | [MapTo("lt")] 24 | LessThan, 25 | [MapTo("lte")] 26 | LessThanOrEqual, 27 | [MapTo("neq")] 28 | NotEqual, 29 | [MapTo("like")] 30 | Like, 31 | [MapTo("ilike")] 32 | ILike, 33 | [MapTo("in")] 34 | In, 35 | [MapTo("is")] 36 | Is, 37 | [MapTo("fts")] 38 | FTS, 39 | [MapTo("plfts")] 40 | PLFTS, 41 | [MapTo("phfts")] 42 | PHFTS, 43 | [MapTo("wfts")] 44 | WFTS, 45 | [MapTo("cs")] 46 | Contains, 47 | [MapTo("cd")] 48 | ContainedIn, 49 | [MapTo("ov")] 50 | Overlap, 51 | [MapTo("sl")] 52 | StrictlyLeft, 53 | [MapTo("sr")] 54 | StrictlyRight, 55 | [MapTo("nxr")] 56 | NotRightOf, 57 | [MapTo("nxl")] 58 | NotLeftOf, 59 | [MapTo("adj")] 60 | Adjacent, 61 | [MapTo("not")] 62 | Not, 63 | } 64 | 65 | public enum Ordering 66 | { 67 | [MapTo("asc")] 68 | Ascending, 69 | [MapTo("desc")] 70 | Descending, 71 | } 72 | 73 | /// 74 | /// See: https://postgrest.org/en/v7.0.0/api.html?highlight=nulls%20first#ordering 75 | /// 76 | public enum NullPosition 77 | { 78 | [MapTo("nullsfirst")] 79 | First, 80 | [MapTo("nullslast")] 81 | Last 82 | } 83 | 84 | /// 85 | /// See: https://postgrest.org/en/v7.0.0/api.html?highlight=count#estimated-count 86 | /// 87 | public enum CountType 88 | { 89 | [MapTo("exact")] 90 | Exact, 91 | [MapTo("planned")] 92 | Planned, 93 | [MapTo("estimated")] 94 | Estimated 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Postgrest/Converters/DateTimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | namespace Supabase.Postgrest.Converters 7 | { 8 | 9 | /// 10 | public class DateTimeConverter : JsonConverter 11 | { 12 | /// 13 | public override bool CanConvert(Type objectType) 14 | { 15 | throw new NotImplementedException(); 16 | } 17 | 18 | /// 19 | public override bool CanWrite => false; 20 | 21 | /// 22 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 23 | { 24 | if (reader.Value != null) 25 | { 26 | var str = reader.Value.ToString(); 27 | 28 | var infinity = ParseInfinity(str); 29 | 30 | if (infinity != null) 31 | { 32 | return (DateTime)infinity; 33 | } 34 | 35 | var date = DateTime.Parse(str); 36 | return date; 37 | } 38 | 39 | var result = new List(); 40 | 41 | try 42 | { 43 | var jo = JArray.Load(reader); 44 | 45 | foreach (var item in jo.ToArray()) 46 | { 47 | var inner = item.ToString(); 48 | 49 | var infinity = ParseInfinity(inner); 50 | 51 | if (infinity != null) 52 | { 53 | result.Add((DateTime)infinity); 54 | } 55 | 56 | var date = DateTime.Parse(inner); 57 | result.Add(date); 58 | } 59 | } 60 | catch (JsonReaderException) 61 | { 62 | return null; 63 | } 64 | 65 | 66 | return result; 67 | } 68 | 69 | private static DateTime? ParseInfinity(string input) 70 | { 71 | if (input.Contains("infinity")) 72 | { 73 | return input.Contains("-") ? DateTime.MinValue : DateTime.MaxValue; 74 | } 75 | 76 | return null; 77 | } 78 | 79 | /// 80 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 81 | { 82 | throw new NotImplementedException(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Postgrest/Converters/IntConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | namespace Supabase.Postgrest.Converters 5 | { 6 | 7 | /// 8 | public class IntArrayConverter : JsonConverter 9 | { 10 | /// 11 | public override bool CanConvert(Type objectType) 12 | { 13 | throw new NotImplementedException(); 14 | } 15 | 16 | /// 17 | public override bool CanRead => false; 18 | 19 | /// 20 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 21 | { 22 | throw new NotImplementedException(); 23 | } 24 | 25 | /// 26 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 27 | { 28 | if (value is List list) 29 | { 30 | writer.WriteValue($"{{{string.Join(",", list)}}}"); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Postgrest/Converters/RangeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Newtonsoft.Json; 4 | using Supabase.Postgrest.Extensions; 5 | using Supabase.Postgrest.Exceptions; 6 | 7 | namespace Supabase.Postgrest.Converters 8 | { 9 | 10 | /// 11 | /// Used by Newtonsoft.Json to convert a C# range into a Postgrest range. 12 | /// 13 | internal class RangeConverter : JsonConverter 14 | { 15 | public override bool CanConvert(Type objectType) 16 | { 17 | throw new NotImplementedException(); 18 | } 19 | 20 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 21 | { 22 | return reader.Value != null ? ParseIntRange(reader.Value.ToString()) : null; 23 | } 24 | 25 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 26 | { 27 | if (value == null) return; 28 | 29 | var val = (IntRange)value; 30 | writer.WriteValue(val.ToPostgresString()); 31 | } 32 | 33 | public static IntRange ParseIntRange(string value) 34 | { 35 | //int4range (0,1] , [123,4123], etc. etc. 36 | const string pattern = @"^(\[|\()(\d+),(\d+)(\]|\))$"; 37 | var matches = Regex.Matches(value, pattern); 38 | 39 | if (matches.Count <= 0) 40 | throw new PostgrestException("Unknown Range format.") { Reason = FailureHint.Reason.InvalidArgument }; 41 | 42 | var groups = matches[0].Groups; 43 | var isInclusiveLower = groups[1].Value == "["; 44 | var isInclusiveUpper = groups[4].Value == "]"; 45 | var value1 = int.Parse(groups[2].Value); 46 | var value2 = int.Parse(groups[3].Value); 47 | 48 | var start = isInclusiveLower ? value1 : value1 + 1; 49 | var count = isInclusiveUpper ? value2 : value2 - 1; 50 | 51 | // Edge-case, includes no points 52 | return count < start ? new IntRange(0, 0) : new IntRange(start, count); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Postgrest/Debugger.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Supabase.Postgrest.Exceptions; 3 | using Supabase.Postgrest.Interfaces; 4 | 5 | namespace Supabase.Postgrest 6 | { 7 | 8 | /// 9 | /// A Singleton used for debug notifications 10 | /// 11 | internal class Debugger 12 | { 13 | private static Debugger? _instance { get; set; } 14 | 15 | /// 16 | /// Returns the Singleton Instance. 17 | /// 18 | public static Debugger Instance 19 | { 20 | get 21 | { 22 | _instance ??= new Debugger(); 23 | return _instance; 24 | } 25 | } 26 | 27 | private Debugger() 28 | { } 29 | 30 | private readonly List _debugListeners = new(); 31 | 32 | /// 33 | /// Adds a debug listener 34 | /// 35 | /// 36 | public void AddDebugHandler(IPostgrestDebugger.DebugEventHandler handler) 37 | { 38 | if (!_debugListeners.Contains(handler)) 39 | _debugListeners.Add(handler); 40 | } 41 | 42 | /// 43 | /// Removes a debug handler. 44 | /// 45 | /// 46 | public void RemoveDebugHandler(IPostgrestDebugger.DebugEventHandler handler) 47 | { 48 | if (_debugListeners.Contains(handler)) 49 | _debugListeners.Remove(handler); 50 | } 51 | 52 | /// 53 | /// Clears debug handlers. 54 | /// 55 | public void ClearDebugHandlers() => 56 | _debugListeners.Clear(); 57 | 58 | /// 59 | /// Notifies debug listeners. 60 | /// 61 | /// 62 | /// 63 | /// 64 | public void Log(object? sender, string message, PostgrestException? exception = null) 65 | { 66 | foreach (var l in _debugListeners.ToArray()) 67 | l.Invoke(sender, message, exception); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Postgrest/Exceptions/FailureHint.cs: -------------------------------------------------------------------------------- 1 | using static Supabase.Postgrest.Exceptions.FailureHint.Reason; 2 | #pragma warning disable CS1591 3 | 4 | namespace Supabase.Postgrest.Exceptions 5 | { 6 | 7 | /// 8 | /// https://postgrest.org/en/v10.2/errors.html?highlight=exception#http-status-codes 9 | /// 10 | public static class FailureHint 11 | { 12 | public enum Reason 13 | { 14 | Unknown, 15 | NotAuthorized, 16 | ForeignKeyViolation, 17 | UniquenessViolation, 18 | ServerError, 19 | UndefinedTable, 20 | UndefinedFunction, 21 | InvalidArgument 22 | } 23 | 24 | public static Reason DetectReason(PostgrestException pgex) 25 | { 26 | if (pgex.Content == null) 27 | return Unknown; 28 | 29 | return pgex.StatusCode switch 30 | { 31 | 401 => NotAuthorized, 32 | 403 when pgex.Content.Contains("apikey") => NotAuthorized, 33 | 404 when pgex.Content.Contains("42883") => UndefinedTable, 34 | 404 when pgex.Content.Contains("42P01") => UndefinedFunction, 35 | 409 when pgex.Content.Contains("23503") => ForeignKeyViolation, 36 | 409 when pgex.Content.Contains("23505") => UniquenessViolation, 37 | 500 => ServerError, 38 | _ => Unknown 39 | }; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Postgrest/Exceptions/PostgrestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | namespace Supabase.Postgrest.Exceptions 4 | { 5 | /// 6 | /// Errors from Postgrest are wrapped by this exception 7 | /// 8 | public class PostgrestException : Exception 9 | { 10 | /// 11 | public PostgrestException(string? message) : base(message) { } 12 | /// 13 | public PostgrestException(string? message, Exception? innerException) : base(message, innerException) { } 14 | 15 | /// 16 | /// The response object from Postgrest 17 | /// 18 | public HttpResponseMessage? Response { get; internal set; } 19 | 20 | /// 21 | /// The content of the response object from Postgrest 22 | /// 23 | public string? Content { get; internal set; } 24 | 25 | /// 26 | /// The HTTP status code of the response object from Postgrest 27 | /// 28 | public int StatusCode { get; internal set; } 29 | 30 | /// 31 | /// Postgres client's best effort at decoding the error from the GoTrue server. 32 | /// 33 | public FailureHint.Reason Reason { get; internal set; } 34 | 35 | /// 36 | /// Attempts to decode the error from the GoTrue server. 37 | /// 38 | public void AddReason() 39 | { 40 | Reason = FailureHint.DetectReason(this); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Postgrest/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Supabase.Postgrest.Extensions 3 | { 4 | 5 | /// 6 | /// Adds functionality to get a typed Attribute attached to an enum value. 7 | /// 8 | public static class EnumExtensions 9 | { 10 | /// 11 | /// Gets a typed Attribute attached to an enum value. 12 | /// 13 | /// 14 | /// 15 | /// 16 | internal static T? GetAttribute(this Enum value) where T : Attribute 17 | { 18 | var type = value.GetType(); 19 | var name = Enum.GetName(type, value); 20 | 21 | if (name == null) 22 | { 23 | return null; 24 | } 25 | 26 | var fieldInfo = type.GetField(name); 27 | 28 | if (fieldInfo == null) 29 | { 30 | return null; 31 | } 32 | 33 | if (Attribute.GetCustomAttribute(fieldInfo, typeof(T)) is T attribute) 34 | { 35 | return attribute; 36 | } 37 | 38 | return null; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Postgrest/Extensions/RangeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Supabase.Postgrest.Extensions 2 | { 3 | 4 | /// 5 | /// Adds functionality to transform a C# Range to a Postgrest String. 6 | /// 7 | /// 8 | /// https://www.postgresql.org/docs/14/rangetypes.html 9 | /// 10 | /// 11 | public static class RangeExtensions 12 | { 13 | /// 14 | /// Transforms a C# Range to a Postgrest String. 15 | /// 16 | /// 17 | /// 18 | internal static string ToPostgresString(this IntRange range) => $"[{range.Start},{range.End}]"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Postgrest/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | namespace Supabase.Postgrest.Extensions 4 | { 5 | internal static class TypeExtensions 6 | { 7 | internal static IEnumerable GetInheritanceHierarchy(this Type type) 8 | { 9 | for (var current = type; current != null; current = current.BaseType) 10 | yield return current; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Postgrest/Extensions/UriExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Supabase.Postgrest.Extensions 3 | { 4 | /// 5 | /// Pull the instance info out of the Uri 6 | /// 7 | public static class UriExtensions 8 | { 9 | /// 10 | /// Pull the instance info out of the Uri 11 | /// 12 | /// 13 | /// 14 | public static string GetInstanceUrl(this Uri uri) => 15 | uri.GetLeftPart(UriPartial.Authority) + uri.LocalPath; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Postgrest/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Web; 9 | using Newtonsoft.Json; 10 | using Newtonsoft.Json.Linq; 11 | using Supabase.Core; 12 | using Supabase.Core.Extensions; 13 | using Supabase.Postgrest.Exceptions; 14 | using Supabase.Postgrest.Models; 15 | using Supabase.Postgrest.Responses; 16 | 17 | [assembly: InternalsVisibleTo("PostgrestTests")] 18 | 19 | namespace Supabase.Postgrest 20 | { 21 | 22 | internal static class Helpers 23 | { 24 | private static readonly HttpClient Client = new HttpClient(); 25 | 26 | private static readonly Guid AppSession = Guid.NewGuid(); 27 | 28 | /// 29 | /// Helper to make a request using the defined parameters to an API Endpoint and coerce into a model. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | public static async Task> MakeRequest(ClientOptions clientOptions, HttpMethod method, string url, JsonSerializerSettings serializerSettings, object? data = null, 42 | Dictionary? headers = null, Func>? getHeaders = null, CancellationToken cancellationToken = default) where T : BaseModel, new() 43 | { 44 | var baseResponse = await MakeRequest(clientOptions, method, url, serializerSettings, data, headers, cancellationToken); 45 | return new ModeledResponse(baseResponse, serializerSettings, getHeaders); 46 | } 47 | 48 | /// 49 | /// Helper to make a request using the defined parameters to an API Endpoint. 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | public static async Task MakeRequest(ClientOptions clientOptions, HttpMethod method, string url, JsonSerializerSettings serializerSettings, object? data = null, 60 | Dictionary? headers = null, CancellationToken cancellationToken = default) 61 | { 62 | var builder = new UriBuilder(url); 63 | var query = HttpUtility.ParseQueryString(builder.Query); 64 | 65 | if (data != null && method == HttpMethod.Get) 66 | { 67 | // Case if it's a Get request the data object is a dictionary 68 | if (data is Dictionary reqParams) 69 | { 70 | foreach (var param in reqParams) 71 | query[param.Key] = param.Value; 72 | } 73 | } 74 | 75 | builder.Query = query.ToString(); 76 | 77 | using var requestMessage = new HttpRequestMessage(method, builder.Uri); 78 | 79 | if (data != null && method != HttpMethod.Get) 80 | { 81 | var stringContent = JsonConvert.SerializeObject(data, serializerSettings); 82 | 83 | if (!string.IsNullOrWhiteSpace(stringContent) && JToken.Parse(stringContent).HasValues) 84 | { 85 | requestMessage.Content = new StringContent(stringContent, Encoding.UTF8, "application/json"); 86 | } 87 | } 88 | 89 | if (headers != null) 90 | { 91 | foreach (var kvp in headers) 92 | { 93 | requestMessage.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); 94 | } 95 | } 96 | 97 | using var response = await Client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); 98 | var content = await response.Content.ReadAsStringAsync(); 99 | 100 | if (response.IsSuccessStatusCode) 101 | return new BaseResponse(clientOptions, response, content); 102 | 103 | var exception = new PostgrestException(content) 104 | { 105 | Content = content, 106 | Response = response, 107 | StatusCode = (int)response.StatusCode 108 | }; 109 | exception.AddReason(); 110 | throw exception; 111 | } 112 | 113 | /// 114 | /// Prepares the request with appropriate HTTP headers expected by Postgrest. 115 | /// 116 | /// 117 | /// 118 | /// 119 | /// 120 | /// 121 | /// 122 | public static Dictionary PrepareRequestHeaders(HttpMethod method, Dictionary? headers = null, ClientOptions? options = null, int rangeFrom = int.MinValue, int rangeTo = int.MinValue) 123 | { 124 | options ??= new ClientOptions(); 125 | 126 | headers = headers == null ? new Dictionary(options.Headers) : options.Headers.MergeLeft(headers); 127 | 128 | if (!string.IsNullOrEmpty(options.Schema)) 129 | { 130 | headers.Add(method == HttpMethod.Get ? "Accept-Profile" : "Content-Profile", options.Schema); 131 | } 132 | 133 | if (rangeFrom != int.MinValue) 134 | { 135 | var formatRangeTo = rangeTo != int.MinValue ? rangeTo.ToString() : null; 136 | 137 | headers.Add("Range-Unit", "items"); 138 | headers.Add("Range", $"{rangeFrom}-{formatRangeTo}"); 139 | } 140 | 141 | if (!headers.ContainsKey("X-Client-Info")) 142 | { 143 | try 144 | { 145 | // Default version to match other clients 146 | // https://github.com/search?q=org%3Asupabase-community+x-client-info&type=code 147 | headers.Add("X-Client-Info", $"postgrest-csharp/{Util.GetAssemblyVersion(typeof(Client))}"); 148 | } 149 | catch (Exception) 150 | { 151 | // Fallback for when the version can't be found 152 | // e.g. running in the Unity Editor, ILL2CPP builds, etc. 153 | headers.Add("X-Client-Info", $"postgrest-csharp/session-{AppSession}"); 154 | } 155 | } 156 | 157 | return headers; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Postgrest/Hooks.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using Newtonsoft.Json; 5 | 6 | namespace Supabase.Postgrest 7 | { 8 | /// 9 | /// Delegate representing the request to be sent to the remote server. 10 | /// 11 | public delegate void OnRequestPreparedEventHandler(object sender, ClientOptions clientOptions, 12 | HttpMethod method, string url, 13 | JsonSerializerSettings serializerSettings, object? data = null, 14 | Dictionary? headers = null); 15 | 16 | /// 17 | /// A internal singleton used for hooks applied to and 18 | /// 19 | internal class Hooks 20 | { 21 | private static Hooks? _instance { get; set; } 22 | 23 | /// 24 | /// Returns the Singleton Instance. 25 | /// 26 | public static Hooks Instance 27 | { 28 | get 29 | { 30 | _instance ??= new Hooks(); 31 | return _instance; 32 | } 33 | } 34 | 35 | private readonly List _requestPreparedEventHandlers = 36 | new List(); 37 | 38 | private Hooks() 39 | { 40 | } 41 | 42 | /// 43 | /// Adds a handler that is called prior to a request being sent. 44 | /// 45 | /// 46 | public void AddRequestPreparedHandler(OnRequestPreparedEventHandler handler) 47 | { 48 | if (!_requestPreparedEventHandlers.Contains(handler)) 49 | _requestPreparedEventHandlers.Add(handler); 50 | } 51 | 52 | /// 53 | /// Removes an handler. 54 | /// 55 | /// 56 | public void RemoveRequestPreparedHandler(OnRequestPreparedEventHandler handler) 57 | { 58 | if (_requestPreparedEventHandlers.Contains(handler)) 59 | _requestPreparedEventHandlers.Remove(handler); 60 | } 61 | 62 | /// 63 | /// Clears all handlers. 64 | /// 65 | public void ClearRequestPreparedHandlers() 66 | { 67 | _requestPreparedEventHandlers.Clear(); 68 | } 69 | 70 | /// 71 | /// Notifies all listeners. 72 | /// 73 | /// 74 | /// 75 | /// 76 | /// 77 | /// 78 | /// 79 | /// 80 | public void NotifyOnRequestPreparedHandlers(object sender, ClientOptions clientOptions, HttpMethod method, 81 | string url, 82 | JsonSerializerSettings serializerSettings, object? data = null, 83 | Dictionary? headers = null) 84 | { 85 | Debugger.Instance.Log(this, $"{nameof(NotifyOnRequestPreparedHandlers)} called for [{method}] to {url}"); 86 | 87 | foreach (var handler in _requestPreparedEventHandlers.ToList()) 88 | handler.Invoke(sender, clientOptions, method, url, serializerSettings, data, headers); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Postgrest/IntRange.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Supabase.Postgrest; 4 | 5 | // https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Index.cs 6 | // https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Range.cs 7 | namespace Supabase.Postgrest 8 | { 9 | /// Represent a type can be used to index a collection either from the start or the end. 10 | /// 11 | /// Index is used by the C# compiler to support the new index syntax 12 | /// 13 | /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; 14 | /// int lastElement = someArray[^1]; // lastElement = 5 15 | /// 16 | /// 17 | public class Index : IEquatable 18 | { 19 | private readonly int _value; 20 | 21 | /// Construct an Index using a value and indicating if the index is from the start or from the end. 22 | /// The index value. it has to be zero or positive number. 23 | /// Indicating if the index is from the start or from the end. 24 | /// 25 | /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. 26 | /// 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public Index(int value, bool fromEnd = false) 29 | { 30 | if (value < 0) 31 | { 32 | throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); 33 | } 34 | 35 | if (fromEnd) 36 | _value = ~value; 37 | else 38 | _value = value; 39 | } 40 | 41 | // The following private constructors mainly created for perf reason to avoid the checks 42 | private Index(int value) 43 | { 44 | _value = value; 45 | } 46 | 47 | /// Create an Index pointing at first element. 48 | public static Index Start => new Index(0); 49 | 50 | /// Create an Index pointing at beyond last element. 51 | public static Index End => new Index(~0); 52 | 53 | /// Create an Index from the start at the position indicated by the value. 54 | /// The index value from the start. 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static Index FromStart(int value) 57 | { 58 | if (value < 0) 59 | { 60 | throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); 61 | } 62 | 63 | return new Index(value); 64 | } 65 | 66 | /// Create an Index from the end at the position indicated by the value. 67 | /// The index value from the end. 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | public static Index FromEnd(int value) 70 | { 71 | if (value < 0) 72 | { 73 | throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); 74 | } 75 | 76 | return new Index(~value); 77 | } 78 | 79 | /// Returns the index value. 80 | public int Value 81 | { 82 | get 83 | { 84 | if (_value < 0) 85 | { 86 | return ~_value; 87 | } 88 | else 89 | { 90 | return _value; 91 | } 92 | } 93 | } 94 | 95 | /// Indicates whether the index is from the start or the end. 96 | public bool IsFromEnd => _value < 0; 97 | 98 | /// Calculate the offset from the start using the giving collection length. 99 | /// The length of the collection that the Index will be used with. length has to be a positive value 100 | /// 101 | /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. 102 | /// we don't validate either the returned offset is greater than the input length. 103 | /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and 104 | /// then used to index a collection will get out of range exception which will be same affect as the validation. 105 | /// 106 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 107 | public int GetOffset(int length) 108 | { 109 | var offset = _value; 110 | if (IsFromEnd) 111 | { 112 | // offset = length - (~value) 113 | // offset = length + (~(~value) + 1) 114 | // offset = length + value + 1 115 | 116 | offset += length + 1; 117 | } 118 | return offset; 119 | } 120 | 121 | /// Indicates whether the current Index object is equal to another object of the same type. 122 | /// An object to compare with this object 123 | public override bool Equals(object? value) => value is Index index && _value == index._value; 124 | 125 | /// Indicates whether the current Index object is equal to another Index object. 126 | /// An object to compare with this object 127 | public bool Equals(Index other) => _value == other._value; 128 | 129 | /// Returns the hash code for this instance. 130 | public override int GetHashCode() => _value; 131 | 132 | /// Converts integer number to an Index. 133 | public static implicit operator Index(int value) => FromStart(value); 134 | 135 | /// Converts the value of the current Index object to its equivalent string representation. 136 | public override string ToString() 137 | { 138 | if (IsFromEnd) 139 | return "^" + ((uint)Value).ToString(); 140 | 141 | return ((uint)Value).ToString(); 142 | } 143 | } 144 | 145 | /// Represent a range has start and end indexes. 146 | /// 147 | /// Range is used by the C# compiler to support the range syntax. 148 | /// 149 | /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; 150 | /// int[] subArray1 = someArray[0..2]; // { 1, 2 } 151 | /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } 152 | /// 153 | /// 154 | public class IntRange : IEquatable 155 | { 156 | /// Represent the inclusive start index of the Range. 157 | public Index Start { get; } 158 | 159 | /// Represent the exclusive end index of the Range. 160 | public Index End { get; } 161 | 162 | /// Construct a Range object using the start and end indexes. 163 | /// Represent the inclusive start index of the range. 164 | /// Represent the exclusive end index of the range. 165 | public IntRange(Index start, Index end) 166 | { 167 | Start = start; 168 | End = end; 169 | } 170 | 171 | /// Indicates whether the current Range object is equal to another object of the same type. 172 | /// An object to compare with this object 173 | public override bool Equals(object? value) => 174 | value is IntRange r && 175 | r.Start.Equals(Start) && 176 | r.End.Equals(End); 177 | 178 | /// Indicates whether the current Range object is equal to another Range object. 179 | /// An object to compare with this object 180 | public bool Equals(IntRange other) => other.Start.Equals(Start) && other.End.Equals(End); 181 | 182 | /// Returns the hash code for this instance. 183 | public override int GetHashCode() 184 | { 185 | return Start.GetHashCode() * 31 + End.GetHashCode(); 186 | } 187 | 188 | /// Converts the value of the current Range object to its equivalent string representation. 189 | public override string ToString() 190 | { 191 | return Start + ".." + End; 192 | } 193 | 194 | /// Create a Range object starting from start index to the end of the collection. 195 | public static IntRange StartAt(Index start) => new IntRange(start, Index.End); 196 | 197 | /// Create a Range object starting from first element in the collection to the end Index. 198 | public static IntRange EndAt(Index end) => new IntRange(Index.Start, end); 199 | 200 | /// Create a Range object starting from first element to the end. 201 | public static IntRange All => new IntRange(Index.Start, Index.End); 202 | 203 | /// Calculate the start offset and length of range object using a collection length. 204 | /// The length of the collection that the range will be used with. length has to be a positive value. 205 | /// 206 | /// For performance reason, we don't validate the input length parameter against negative values. 207 | /// It is expected Range will be used with collections which always have non negative length/count. 208 | /// We validate the range is inside the length scope though. 209 | /// 210 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 211 | public (int Offset, int Length) GetOffsetAndLength(int length) 212 | { 213 | int start; 214 | var startIndex = Start; 215 | if (startIndex.IsFromEnd) 216 | start = length - startIndex.Value; 217 | else 218 | start = startIndex.Value; 219 | 220 | int end; 221 | var endIndex = End; 222 | if (endIndex.IsFromEnd) 223 | end = length - endIndex.Value; 224 | else 225 | end = endIndex.Value; 226 | 227 | if ((uint)end > (uint)length || (uint)start > (uint)end) 228 | { 229 | throw new ArgumentOutOfRangeException(nameof(length)); 230 | } 231 | 232 | return (start, end - start); 233 | } 234 | } 235 | } 236 | 237 | namespace System.Runtime.CompilerServices 238 | { 239 | internal static class RuntimeHelpers 240 | { 241 | /// 242 | /// Slices the specified array using the specified range. 243 | /// 244 | public static T[] GetSubArray(T[] array, IntRange range) 245 | { 246 | if (array == null) 247 | { 248 | throw new ArgumentNullException(nameof(array)); 249 | } 250 | 251 | (int offset, int length) = range.GetOffsetAndLength(array.Length); 252 | 253 | if (default(T) != null || typeof(T[]) == array.GetType()) 254 | { 255 | // We know the type of the array to be exactly T[]. 256 | 257 | if (length == 0) 258 | { 259 | return Array.Empty(); 260 | } 261 | 262 | var dest = new T[length]; 263 | Array.Copy(array, offset, dest, 0, length); 264 | return dest; 265 | } 266 | else 267 | { 268 | // The array is actually a U[] where U:T. 269 | var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); 270 | Array.Copy(array, offset, dest, 0, length); 271 | return dest; 272 | } 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /Postgrest/Interfaces/IPostgrestCacheProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Supabase.Postgrest.Interfaces 4 | { 5 | /// 6 | /// A caching provider than can be used by postgrest to store requests. 7 | /// 8 | public interface IPostgrestCacheProvider 9 | { 10 | /// 11 | /// Gets an item from a caching solution, should coerce into a datatype. 12 | /// 13 | /// This will most likely be a JSON deserialization approach. 14 | /// 15 | /// A reproducible key for a defined query. 16 | /// 17 | /// 18 | public Task GetItem(string key); 19 | 20 | /// 21 | /// Sets an item within a caching solution, should store in a way that the data can be retrieved and coerced into a generic type by 22 | /// 23 | /// This will most likely be a JSON serialization approach. 24 | /// 25 | /// A reproducible key for a defined query. 26 | /// An object of serializable data. 27 | /// 28 | public Task SetItem(string key, object value); 29 | 30 | /// 31 | /// Clear an item within a caching solution by a key. 32 | /// 33 | /// A reproducible key for a defined query. 34 | /// 35 | public Task ClearItem(string key); 36 | 37 | /// 38 | /// An empty/clear cache implementation. 39 | /// 40 | /// 41 | public Task Empty(); 42 | } 43 | } -------------------------------------------------------------------------------- /Postgrest/Interfaces/IPostgrestClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Supabase.Core.Interfaces; 3 | using Supabase.Postgrest.Models; 4 | using Supabase.Postgrest.Responses; 5 | 6 | namespace Supabase.Postgrest.Interfaces 7 | { 8 | /// 9 | /// Client interface for Postgrest 10 | /// 11 | public interface IPostgrestClient : IGettableHeaders 12 | { 13 | /// 14 | /// API Base Url for subsequent calls. 15 | /// 16 | string BaseUrl { get; } 17 | 18 | /// 19 | /// The Options was initialized with. 20 | /// 21 | ClientOptions Options { get; } 22 | 23 | /// 24 | /// Adds a handler that is called prior to a request being sent. 25 | /// 26 | /// 27 | void AddRequestPreparedHandler(OnRequestPreparedEventHandler handler); 28 | 29 | /// 30 | /// Removes an handler. 31 | /// 32 | /// 33 | void RemoveRequestPreparedHandler(OnRequestPreparedEventHandler handler); 34 | 35 | /// 36 | /// Clears all handlers. 37 | /// 38 | void ClearRequestPreparedHandlers(); 39 | 40 | /// 41 | /// Adds a debug handler 42 | /// 43 | /// 44 | void AddDebugHandler(IPostgrestDebugger.DebugEventHandler handler); 45 | 46 | /// 47 | /// Removes a debug handler 48 | /// 49 | /// /// 50 | void RemoveDebugHandler(IPostgrestDebugger.DebugEventHandler handler); 51 | 52 | /// 53 | /// Clears debug handlers 54 | /// 55 | void ClearDebugHandlers(); 56 | 57 | /// 58 | /// Perform a stored procedure call. 59 | /// 60 | /// The function name to call 61 | /// The parameters to pass to the function call 62 | /// 63 | Task Rpc(string procedureName, object? parameters); 64 | 65 | /// 66 | /// Perform a stored procedure call. 67 | /// 68 | /// The function name to call 69 | /// The parameters to pass to the function call 70 | /// A type used for hydrating the HTTP response content (hydration through JSON.NET) 71 | /// A hydrated model 72 | Task Rpc(string procedureName, object? parameters = null); 73 | 74 | /// 75 | /// Returns a Table Query Builder instance for a defined model - representative of `USE $TABLE` 76 | /// 77 | /// Custom Model derived from `BaseModel` 78 | /// 79 | IPostgrestTable Table() where T : BaseModel, new(); 80 | 81 | /// 82 | /// Returns a Table Query Builder instance with a Cache Provider for a defined model - representative of `USE #$TABLE` 83 | /// 84 | /// 85 | /// 86 | /// 87 | IPostgrestTableWithCache Table(IPostgrestCacheProvider cacheProvider) where T : BaseModel, new(); 88 | } 89 | } -------------------------------------------------------------------------------- /Postgrest/Interfaces/IPostgrestDebugger.cs: -------------------------------------------------------------------------------- 1 | using Supabase.Postgrest.Exceptions; 2 | 3 | namespace Supabase.Postgrest.Interfaces 4 | { 5 | /// 6 | /// Interface for getting debug info from Postgrest 7 | /// 8 | public interface IPostgrestDebugger 9 | { 10 | /// 11 | delegate void DebugEventHandler(object? sender, string message, PostgrestException? exception); 12 | 13 | /// 14 | /// Adds a debug handler 15 | /// 16 | /// 17 | void AddDebugHandler(DebugEventHandler handler); 18 | /// 19 | /// Removes a debug handler 20 | /// 21 | /// 22 | void RemoveDebugHandler(DebugEventHandler handler); 23 | /// 24 | /// Clears debug handlers 25 | /// 26 | void ClearDebugHandlers(); 27 | /// 28 | /// Logs a message 29 | /// 30 | /// 31 | /// 32 | /// 33 | void Log(object? sender, string message, PostgrestException? exception = null); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Postgrest/Interfaces/IPostgrestQueryFilter.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 2 | namespace Supabase.Postgrest.Interfaces 3 | { 4 | public interface IPostgrestQueryFilter 5 | { 6 | object? Criteria { get; } 7 | Constants.Operator Op { get; } 8 | string? Property { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Postgrest/Interfaces/IPostgrestTableWithCache.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Supabase.Postgrest.Models; 4 | using Supabase.Postgrest.Requests; 5 | 6 | namespace Supabase.Postgrest.Interfaces 7 | { 8 | /// 9 | /// Client interface for Postgrest 10 | /// 11 | /// 12 | public interface IPostgrestTableWithCache : IPostgrestTable where T : BaseModel, new() 13 | { 14 | /// 15 | /// Performs a Get request, returning a which populates from the cache, if applicable. 16 | /// 17 | /// 18 | /// 19 | public new Task> Get(CancellationToken cancellationToken = default); 20 | } 21 | } -------------------------------------------------------------------------------- /Postgrest/Linq/SelectExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using Supabase.Postgrest.Attributes; 5 | 6 | namespace Supabase.Postgrest.Linq 7 | 8 | { 9 | 10 | /// 11 | /// Helper class for parsing Select linq queries. 12 | /// 13 | internal class SelectExpressionVisitor : ExpressionVisitor 14 | { 15 | /// 16 | /// The columns that have been selected from this linq expression. 17 | /// 18 | public List Columns { get; } = new(); 19 | 20 | /// 21 | /// The root call that will be looped through to populate . 22 | /// 23 | /// Called like: `Table<Movies>().Select(x => new[] { x.Id, x.Name, x.CreatedAt }).Get()` 24 | /// 25 | /// 26 | /// 27 | protected override Expression VisitNewArray(NewArrayExpression node) 28 | { 29 | foreach (var expression in node.Expressions) 30 | Visit(expression); 31 | 32 | return node; 33 | } 34 | 35 | /// 36 | /// A Member Node, representing a property on a BaseModel. 37 | /// 38 | /// 39 | /// 40 | protected override Expression VisitMember(MemberExpression node) 41 | { 42 | var column = GetColumnFromMemberExpression(node); 43 | 44 | if (column != null) 45 | Columns.Add(column); 46 | 47 | return node; 48 | } 49 | 50 | /// 51 | /// A Unary Node, delved into to represent a property on a BaseModel. 52 | /// 53 | /// 54 | /// 55 | protected override Expression VisitUnary(UnaryExpression node) 56 | { 57 | if (node.Operand is MemberExpression memberExpression) 58 | { 59 | var column = GetColumnFromMemberExpression(memberExpression); 60 | 61 | if (column != null) 62 | Columns.Add(column); 63 | } 64 | 65 | return node; 66 | } 67 | 68 | /// 69 | /// Gets a column name from property based on it's supplied attributes. 70 | /// 71 | /// 72 | /// 73 | private string? GetColumnFromMemberExpression(MemberExpression node) 74 | { 75 | var type = node.Member.ReflectedType; 76 | var prop = type?.GetProperty(node.Member.Name); 77 | var attrs = prop?.GetCustomAttributes(true); 78 | 79 | if (attrs == null) 80 | throw new ArgumentException($"Unknown argument '{node.Member.Name}' provided, does it have a `Column` or `PrimaryKey` attribute?"); 81 | 82 | foreach (var attr in attrs) 83 | { 84 | switch (attr) 85 | { 86 | case ColumnAttribute columnAttr: 87 | return columnAttr.ColumnName; 88 | case PrimaryKeyAttribute primaryKeyAttr: 89 | return primaryKeyAttr.ColumnName; 90 | } 91 | } 92 | 93 | throw new ArgumentException($"Unknown argument '{node.Member.Name}' provided, does it have a `Column` or `PrimaryKey` attribute?"); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Postgrest/Linq/SetExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using Supabase.Postgrest.Attributes; 5 | 6 | namespace Supabase.Postgrest.Linq 7 | 8 | { 9 | /// 10 | /// Helper class for parsing Set linq queries. 11 | /// 12 | internal class SetExpressionVisitor : ExpressionVisitor 13 | { 14 | /// 15 | /// The column that have been selected from this linq expression. 16 | /// 17 | public string? Column { get; private set; } 18 | 19 | /// 20 | /// The Column's type that value should be checked against. 21 | /// 22 | public Type? ExpectedType { get; private set; } 23 | 24 | /// 25 | /// Value to be updated. 26 | /// 27 | public object? Value { get; private set; } 28 | 29 | /// 30 | /// A Unary Node, delved into to represent a property on a BaseModel. 31 | /// 32 | /// 33 | /// 34 | protected override Expression VisitUnary(UnaryExpression node) 35 | { 36 | if (node.Operand is not MemberExpression memberExpression) return node; 37 | 38 | var column = GetColumnFromMemberExpression(memberExpression); 39 | 40 | Column = column; 41 | ExpectedType = memberExpression.Type; 42 | 43 | return node; 44 | } 45 | 46 | /// 47 | /// A Member Node, representing a property on a BaseModel. 48 | /// 49 | /// 50 | /// 51 | protected override Expression VisitMember(MemberExpression node) 52 | { 53 | var column = GetColumnFromMemberExpression(node); 54 | 55 | Column = column; 56 | ExpectedType = node.Type; 57 | 58 | return node; 59 | } 60 | 61 | /// 62 | /// Called when visiting a the expected new KeyValuePair(). 63 | /// 64 | /// 65 | /// 66 | /// 67 | protected override Expression VisitNew(NewExpression node) 68 | { 69 | if (typeof(KeyValuePair).IsAssignableFrom(node.Type)) 70 | { 71 | HandleKeyValuePair(node); 72 | } 73 | 74 | return node; 75 | } 76 | 77 | private void HandleKeyValuePair(NewExpression node) 78 | { 79 | if (node.Arguments.Count != 2) 80 | throw new ArgumentException("Unknown expression, should be a `KeyValuePair`"); 81 | 82 | var left = node.Arguments[0]; 83 | var right = node.Arguments[1]; 84 | 85 | if (left is NewExpression) 86 | { 87 | Visit(left); 88 | } 89 | else if (left is MemberExpression member) 90 | { 91 | Column = GetColumnFromMemberExpression(member); 92 | ExpectedType = member.Type; 93 | } 94 | else if (left is UnaryExpression unaryExpression && 95 | unaryExpression.Operand is MemberExpression unaryMemberExpression) 96 | { 97 | Column = GetColumnFromMemberExpression(unaryMemberExpression); 98 | ExpectedType = unaryMemberExpression.Type; 99 | } 100 | else 101 | { 102 | throw new ArgumentException("Key should reference a Model Property."); 103 | } 104 | 105 | var valueArgument = Expression.Lambda(right).Compile().DynamicInvoke(); 106 | Value = valueArgument; 107 | 108 | if (!ExpectedType!.IsInstanceOfType(Value)) 109 | throw new ArgumentException( 110 | $"Expected Value to be of Type: {ExpectedType.Name}, instead received: {Value.GetType().Name}."); 111 | } 112 | 113 | /// 114 | /// Gets a column name from property based on it's supplied attributes. 115 | /// 116 | /// 117 | /// 118 | private string GetColumnFromMemberExpression(MemberExpression node) 119 | { 120 | var type = node.Member.ReflectedType; 121 | var prop = type?.GetProperty(node.Member.Name); 122 | var attrs = prop?.GetCustomAttributes(true); 123 | 124 | if (attrs == null) 125 | throw new ArgumentException( 126 | $"Unknown argument '{node.Member.Name}' provided, does it have a Column or PrimaryKey attribute?"); 127 | 128 | foreach (var attr in attrs) 129 | { 130 | switch (attr) 131 | { 132 | case ColumnAttribute columnAttr: 133 | return columnAttr.ColumnName; 134 | case PrimaryKeyAttribute primaryKeyAttr: 135 | return primaryKeyAttr.ColumnName; 136 | } 137 | } 138 | 139 | throw new ArgumentException( 140 | $"Unknown argument '{node.Member.Name}' provided, does it have a Column or PrimaryKey attribute?"); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /Postgrest/Linq/WhereExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using Supabase.Postgrest.Attributes; 9 | using Supabase.Postgrest.Interfaces; 10 | using static Supabase.Postgrest.Constants; 11 | 12 | // ReSharper disable InvalidXmlDocComment 13 | 14 | namespace Supabase.Postgrest.Linq 15 | 16 | { 17 | /// 18 | /// Helper class for parsing Where linq queries. 19 | /// 20 | internal class WhereExpressionVisitor : ExpressionVisitor 21 | { 22 | /// 23 | /// The filter resulting from this Visitor, capable of producing nested filters. 24 | /// 25 | public QueryFilter? Filter { get; private set; } 26 | 27 | /// 28 | /// An entry point that will be used to populate . 29 | /// 30 | /// Invoked like: 31 | /// `Table<Movies>().Where(x => x.Name == "Top Gun").Get();` 32 | /// 33 | /// 34 | /// 35 | /// 36 | protected override Expression VisitBinary(BinaryExpression node) 37 | { 38 | var op = GetMappedOperator(node); 39 | 40 | // In the event this is a nested expression (n.Name == "Example" || n.Id = 3) 41 | switch (node.NodeType) 42 | { 43 | case ExpressionType.And: 44 | case ExpressionType.Or: 45 | case ExpressionType.AndAlso: 46 | case ExpressionType.OrElse: 47 | var leftVisitor = new WhereExpressionVisitor(); 48 | leftVisitor.Visit(node.Left); 49 | 50 | var rightVisitor = new WhereExpressionVisitor(); 51 | rightVisitor.Visit(node.Right); 52 | 53 | Filter = new QueryFilter(op, 54 | new List { leftVisitor.Filter!, rightVisitor.Filter! }); 55 | 56 | return node; 57 | } 58 | 59 | // Otherwise, the base case. 60 | 61 | var left = Visit(node.Left); 62 | var right = Visit(node.Right); 63 | 64 | string? column = null; 65 | if (left is MemberExpression leftMember) 66 | { 67 | column = GetColumnFromMemberExpression(leftMember); 68 | } //To handle properly if it's a Convert ExpressionType generally with nullable properties 69 | else if (left is UnaryExpression leftUnary && leftUnary.NodeType == ExpressionType.Convert && 70 | leftUnary.Operand is MemberExpression leftOperandMember) 71 | { 72 | column = GetColumnFromMemberExpression(leftOperandMember); 73 | } 74 | 75 | if (column == null) 76 | throw new ArgumentException( 77 | $"Left side of expression: '{node}' is expected to be property with a ColumnAttribute or PrimaryKeyAttribute"); 78 | 79 | if (right is ConstantExpression rightConstant) 80 | { 81 | HandleConstantExpression(column, op, rightConstant); 82 | } 83 | else if (right is MemberExpression memberExpression) 84 | { 85 | HandleMemberExpression(column, op, memberExpression); 86 | } 87 | else if (right is NewExpression newExpression) 88 | { 89 | HandleNewExpression(column, op, newExpression); 90 | } 91 | else if (right is UnaryExpression unaryExpression) 92 | { 93 | HandleUnaryExpression(column, op, unaryExpression); 94 | } 95 | 96 | return node; 97 | } 98 | 99 | /// 100 | /// Called when evaluating a method 101 | /// 102 | /// 103 | /// 104 | /// 105 | /// 106 | protected override Expression VisitMethodCall(MethodCallExpression node) 107 | { 108 | var obj = node.Object as MemberExpression; 109 | 110 | if (obj == null) 111 | throw new ArgumentException( 112 | $"Calling context '{node.Object}' is expected to be a member of or derived from `BaseModel`"); 113 | 114 | var column = GetColumnFromMemberExpression(obj); 115 | 116 | if (column == null) 117 | throw new ArgumentException( 118 | $"Left side of expression: '{node.ToString()}' is expected to be property with a ColumnAttribute or PrimaryKeyAttribute"); 119 | 120 | switch (node.Method.Name) 121 | { 122 | // Includes String.Contains and IEnumerable.Contains 123 | case nameof(String.Contains): 124 | 125 | if (typeof(ICollection).IsAssignableFrom(node.Method.DeclaringType)) 126 | Filter = new QueryFilter(column, Operator.Contains, GetArgumentValues(node)); 127 | else 128 | Filter = new QueryFilter(column, Operator.Like, "*" + GetArgumentValues(node).First() + "*"); 129 | 130 | break; 131 | default: 132 | throw new NotImplementedException("Unsupported method"); 133 | } 134 | 135 | return node; 136 | } 137 | 138 | /// 139 | /// A constant expression parser (i.e. x => x.Id == 5 <- where '5' is the constant) 140 | /// 141 | /// 142 | /// 143 | /// 144 | private void HandleConstantExpression(string column, Operator op, ConstantExpression constantExpression) 145 | { 146 | if (constantExpression.Type.IsEnum) 147 | { 148 | var enumValue = constantExpression.Value; 149 | Filter = new QueryFilter(column, op, enumValue); 150 | } 151 | else 152 | { 153 | Filter = new QueryFilter(column, op, constantExpression.Value); 154 | } 155 | } 156 | 157 | /// 158 | /// A member expression parser (i.e. => x.Id == Example.Id <- where both `x.Id` and `Example.Id` are parsed as 'members') 159 | /// 160 | /// 161 | /// 162 | /// 163 | private void HandleMemberExpression(string column, Operator op, MemberExpression memberExpression) 164 | { 165 | Filter = new QueryFilter(column, op, GetMemberExpressionValue(memberExpression)); 166 | } 167 | 168 | /// 169 | /// A unary expression parser (i.e. => x.Id == 1 <- where both `1` is considered unary) 170 | /// 171 | /// 172 | /// 173 | /// 174 | private void HandleUnaryExpression(string column, Operator op, UnaryExpression unaryExpression) 175 | { 176 | if (unaryExpression.Operand is ConstantExpression constantExpression) 177 | { 178 | HandleConstantExpression(column, op, constantExpression); 179 | } 180 | else if (unaryExpression.Operand is MemberExpression memberExpression) 181 | { 182 | HandleMemberExpression(column, op, memberExpression); 183 | } 184 | else if (unaryExpression.Operand is NewExpression newExpression) 185 | { 186 | HandleNewExpression(column, op, newExpression); 187 | } 188 | } 189 | 190 | /// 191 | /// An instantiated class parser (i.e. x => x.CreatedAt <= new DateTime(2022, 08, 20) <- where `new DateTime(...)` is an instantiated expression. 192 | /// 193 | /// 194 | /// 195 | /// 196 | private void HandleNewExpression(string column, Operator op, NewExpression newExpression) 197 | { 198 | var argumentValues = new List(); 199 | foreach (var argument in newExpression.Arguments) 200 | { 201 | var lambda = Expression.Lambda(argument); 202 | var func = lambda.Compile(); 203 | argumentValues.Add(func.DynamicInvoke()); 204 | } 205 | 206 | var constructor = newExpression.Constructor; 207 | var instance = constructor.Invoke(argumentValues.ToArray()); 208 | 209 | switch (instance) 210 | { 211 | case DateTime dateTime: 212 | Filter = new QueryFilter(column, op, dateTime); 213 | break; 214 | case DateTimeOffset dateTimeOffset: 215 | Filter = new QueryFilter(column, op, dateTimeOffset); 216 | break; 217 | case Guid guid: 218 | Filter = new QueryFilter(column, op, guid.ToString()); 219 | break; 220 | default: 221 | { 222 | if (instance.GetType().IsEnum) 223 | { 224 | Filter = new QueryFilter(column, op, instance); 225 | } 226 | 227 | break; 228 | } 229 | } 230 | } 231 | 232 | /// 233 | /// Gets a column name (postgrest) from a Member Expression (used on BaseModel) 234 | /// 235 | /// 236 | /// 237 | private string GetColumnFromMemberExpression(MemberExpression node) 238 | { 239 | var type = node.Member.ReflectedType; 240 | var prop = type?.GetProperty(node.Member.Name); 241 | var attrs = prop?.GetCustomAttributes(true); 242 | 243 | if (attrs == null) return node.Member.Name; 244 | 245 | foreach (var attr in attrs) 246 | { 247 | switch (attr) 248 | { 249 | case ColumnAttribute columnAttr: 250 | return columnAttr.ColumnName; 251 | case PrimaryKeyAttribute primaryKeyAttr: 252 | return primaryKeyAttr.ColumnName; 253 | } 254 | } 255 | 256 | return node.Member.Name; 257 | } 258 | 259 | /// 260 | /// Get the value from a MemberExpression, which includes both fields and properties. 261 | /// 262 | /// 263 | /// 264 | private object GetMemberExpressionValue(MemberExpression member) 265 | { 266 | if (member.Member is FieldInfo field) 267 | { 268 | var obj = Expression.Lambda(member.Expression).Compile().DynamicInvoke(); 269 | return field.GetValue(obj); 270 | } 271 | 272 | var lambda = Expression.Lambda(member); 273 | var func = lambda.Compile(); 274 | return func.DynamicInvoke(); 275 | } 276 | 277 | /// 278 | /// Creates map between linq and 279 | /// 280 | /// 281 | /// 282 | private Operator GetMappedOperator(Expression node) 283 | { 284 | return node.NodeType switch 285 | { 286 | ExpressionType.Not => Operator.Not, 287 | ExpressionType.And => Operator.And, 288 | ExpressionType.AndAlso => Operator.And, 289 | ExpressionType.OrElse => Operator.Or, 290 | ExpressionType.Or => Operator.Or, 291 | ExpressionType.Equal => Operator.Equals, 292 | ExpressionType.NotEqual => Operator.NotEqual, 293 | ExpressionType.LessThan => Operator.LessThan, 294 | ExpressionType.GreaterThan => Operator.GreaterThan, 295 | ExpressionType.LessThanOrEqual => Operator.LessThanOrEqual, 296 | ExpressionType.GreaterThanOrEqual => Operator.GreaterThanOrEqual, 297 | _ => Operator.Equals 298 | }; 299 | } 300 | 301 | /// 302 | /// Gets arguments from a method call expression, (i.e. x => x.Name.Contains("Top")) <- where `Top` is the argument on the called method `Contains` 303 | /// 304 | /// 305 | /// 306 | List GetArgumentValues(MethodCallExpression methodCall) 307 | { 308 | var argumentValues = new List(); 309 | 310 | foreach (var argument in methodCall.Arguments) 311 | { 312 | var lambda = Expression.Lambda(argument); 313 | var func = lambda.Compile(); 314 | argumentValues.Add(func.DynamicInvoke()); 315 | } 316 | 317 | return argumentValues; 318 | } 319 | } 320 | } -------------------------------------------------------------------------------- /Postgrest/Models/BaseModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Supabase.Postgrest.Attributes; 7 | using Supabase.Postgrest.Exceptions; 8 | using Supabase.Postgrest.Responses; 9 | 10 | #pragma warning disable CS1591 11 | 12 | namespace Supabase.Postgrest.Models 13 | { 14 | 15 | /// 16 | /// Abstract class that must be implemented by C# Postgrest Models. 17 | /// 18 | public abstract class BaseModel 19 | { 20 | [JsonIgnore] 21 | public string? BaseUrl { get; set; } 22 | 23 | [JsonIgnore] 24 | public ClientOptions? RequestClientOptions { get; set; } 25 | 26 | [JsonIgnore] 27 | internal Func>? GetHeaders { get; set; } 28 | 29 | 30 | public Task> Update(CancellationToken cancellationToken = default) where T : BaseModel, new() 31 | { 32 | if (BaseUrl != null) 33 | { 34 | var client = new Client(BaseUrl, RequestClientOptions) 35 | { 36 | GetHeaders = GetHeaders 37 | }; 38 | return client.Table().Update((T)this, cancellationToken: cancellationToken); 39 | } 40 | 41 | throw new PostgrestException("`BaseUrl` should be set in the model."); 42 | } 43 | 44 | public virtual Task Delete(CancellationToken cancellationToken = default) where T : BaseModel, new() 45 | { 46 | if (BaseUrl == null) 47 | throw new PostgrestException("`BaseUrl` should be set in the model.") { Reason = FailureHint.Reason.ServerError }; 48 | 49 | var client = new Client(BaseUrl, RequestClientOptions) 50 | { 51 | GetHeaders = GetHeaders 52 | }; 53 | 54 | return client.Table().Delete((T)this, cancellationToken: cancellationToken); 55 | } 56 | 57 | 58 | [JsonIgnore] 59 | public string TableName 60 | { 61 | get 62 | { 63 | var attribute = Attribute.GetCustomAttribute(GetType(), typeof(TableAttribute)); 64 | 65 | return attribute is TableAttribute tableAttr ? tableAttr.Name : GetType().Name; 66 | } 67 | } 68 | 69 | /// 70 | /// Gets the values of the PrimaryKey columns (there can be multiple) on a model's instance as defined by the [PrimaryKey] attributes on a property on the model. 71 | /// 72 | [JsonIgnore] 73 | public Dictionary PrimaryKey 74 | { 75 | get 76 | { 77 | var result = new Dictionary(); 78 | var propertyInfos = GetType().GetProperties(); 79 | 80 | foreach (var info in propertyInfos) 81 | { 82 | var hasAttr = Attribute.GetCustomAttribute(info, typeof(PrimaryKeyAttribute)); 83 | 84 | if (hasAttr is PrimaryKeyAttribute pka) 85 | { 86 | result.Add(pka, info.GetValue(this)); 87 | } 88 | } 89 | 90 | if (result.Count > 0) 91 | return result; 92 | 93 | throw new PostgrestException("Models must specify their Primary Key via the [PrimaryKey] Attribute") { Reason = FailureHint.Reason.InvalidArgument }; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Postgrest/Models/CachedModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace Supabase.Postgrest.Models 6 | { 7 | /// 8 | /// Represents a cacheable model 9 | /// 10 | /// 11 | public class CachedModel where TModel : BaseModel, new() 12 | { 13 | /// 14 | /// The stored Models 15 | /// 16 | [JsonProperty("response")] public List? Models { get; set; } 17 | 18 | /// 19 | /// Cache time in UTC. 20 | /// 21 | [JsonProperty("cachedAt")] public DateTime CachedAt { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /Postgrest/Postgrest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 9.0 5 | CS8600;CS8602;CS8603 6 | enable 7 | Supabase.Postgrest 8 | Supabase.Postgrest 9 | Supabase.Postgrest 10 | Supabase.Postgrest 11 | Joseph Schultz <joseph@acupofjose.com> 12 | true 13 | MIT 14 | en-US 15 | true 16 | MIT 17 | Joseph Schultz <joseph@acupofjose.com> 18 | https://github.com/supabase-community/postgrest-csharp 19 | Library to interact with postgREST servers 20 | Postgrest-csharp is written primarily as a helper library for supabase/supabase-csharp, however, it should be easy enough to use outside of the supabase ecosystem. 21 | The bulk of this library is a translation and c-sharp-ification of the supabase/postgrest-js library. 22 | 23 | https://avatars.githubusercontent.com/u/54469796?s=200&v=4 24 | supabase,postgrest 25 | 4.1.0 26 | 4.1.0 27 | true 28 | icon.png 29 | README.md 30 | https://github.com/supabase-community/postgrest-csharp 31 | git 32 | true 33 | snupkg 34 | true 35 | 36 | 37 | 38 | 4.1.0 39 | 40 | $(VersionPrefix)-$(VersionSuffix) 41 | $(VersionPrefix) 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Postgrest/PostgrestContractResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Serialization; 7 | using Supabase.Postgrest.Attributes; 8 | using Supabase.Postgrest.Converters; 9 | 10 | namespace Supabase.Postgrest 11 | { 12 | /// 13 | /// A custom resolver that handles mapping column names and property names as well 14 | /// as handling the conversion of Postgrest Ranges to a C# `Range`. 15 | /// 16 | public class PostgrestContractResolver : DefaultContractResolver 17 | { 18 | private bool IsUpdate { get; set; } 19 | private bool IsInsert { get; set; } 20 | private bool IsUpsert { get; set; } 21 | 22 | /// 23 | /// Sets the state of the contract resolver to either insert, update, or upsert. 24 | /// 25 | /// 26 | /// 27 | /// 28 | public void SetState(bool isInsert = false, bool isUpdate = false, bool isUpsert = false) 29 | { 30 | IsUpdate = isUpdate; 31 | IsInsert = isInsert; 32 | IsUpsert = isUpsert; 33 | } 34 | 35 | /// 36 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 37 | { 38 | JsonProperty prop = base.CreateProperty(member, memberSerialization); 39 | 40 | // Handle non-primitive conversions from a Postgres type to C# 41 | if (prop.PropertyType == typeof(IntRange)) 42 | { 43 | prop.Converter = new RangeConverter(); 44 | } 45 | else if (prop.PropertyType != null && (prop.PropertyType == typeof(DateTime) || 46 | Nullable.GetUnderlyingType(prop.PropertyType) == typeof(DateTime))) 47 | { 48 | prop.Converter = new DateTimeConverter(); 49 | } 50 | else if (prop.PropertyType == typeof(List)) 51 | { 52 | prop.Converter = new IntArrayConverter(); 53 | } 54 | else if (prop.PropertyType != null && (prop.PropertyType == typeof(List) || 55 | Nullable.GetUnderlyingType(prop.PropertyType) == 56 | typeof(List))) 57 | { 58 | prop.Converter = new DateTimeConverter(); 59 | } 60 | 61 | // Dynamically set the name of the key we are serializing/deserializing from the model. 62 | if (!member.CustomAttributes.Any()) 63 | { 64 | return prop; 65 | } 66 | 67 | var columnAttribute = member.GetCustomAttribute(); 68 | 69 | if (columnAttribute != null) 70 | { 71 | prop.PropertyName = columnAttribute.ColumnName; 72 | prop.NullValueHandling = columnAttribute.NullValueHandling; 73 | 74 | if (IsInsert && columnAttribute.IgnoreOnInsert) 75 | prop.Ignored = true; 76 | 77 | if (IsUpdate && columnAttribute.IgnoreOnUpdate) 78 | prop.Ignored = true; 79 | 80 | if ((IsUpsert && columnAttribute.IgnoreOnUpdate) || (IsUpsert && columnAttribute.IgnoreOnInsert)) 81 | prop.Ignored = true; 82 | 83 | return prop; 84 | } 85 | 86 | var referenceAttr = member.GetCustomAttribute(); 87 | 88 | if (referenceAttr != null) 89 | { 90 | // If a foreign key is not specified, PostgREST will return JSON that uses the table's name as the key. 91 | prop.PropertyName = string.IsNullOrEmpty(referenceAttr.ForeignKey) 92 | ? referenceAttr.TableName 93 | : referenceAttr.ColumnName; 94 | 95 | if (IsInsert || IsUpdate) 96 | prop.Ignored = true; 97 | 98 | return prop; 99 | } 100 | 101 | var primaryKeyAttribute = member.GetCustomAttribute(); 102 | if (primaryKeyAttribute == null) 103 | return prop; 104 | 105 | prop.PropertyName = primaryKeyAttribute.ColumnName; 106 | prop.ShouldSerialize = instance => primaryKeyAttribute.ShouldInsert || (IsUpsert && instance != null); 107 | return prop; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /Postgrest/QueryFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using Newtonsoft.Json; 8 | using Supabase.Postgrest.Exceptions; 9 | using Supabase.Postgrest.Interfaces; 10 | using Supabase.Postgrest.Linq; 11 | using static Supabase.Postgrest.Constants; 12 | 13 | namespace Supabase.Postgrest 14 | { 15 | /// 16 | /// Allow for the expression of a query filter with linq expressions. 17 | /// 18 | /// 19 | /// 20 | public class QueryFilter : IPostgrestQueryFilter 21 | { 22 | /// 23 | public object? Criteria { get; } 24 | 25 | /// 26 | public Operator Op { get; } 27 | 28 | /// 29 | public string? Property { get; } 30 | 31 | /// 32 | /// Allows the creation of a Query Filter using a LINQ expression. 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | public QueryFilter(Expression> predicate, Operator op, TCriterion? criterion) 39 | { 40 | var visitor = new SelectExpressionVisitor(); 41 | visitor.Visit(predicate); 42 | 43 | if (visitor.Columns.Count == 0) 44 | throw new ArgumentException("Expected predicate to return a reference to a Model column."); 45 | 46 | if (visitor.Columns.Count > 1) 47 | throw new ArgumentException("Only one column should be returned from the predicate."); 48 | 49 | var filter = new QueryFilter(visitor.Columns.First(), op, criterion); 50 | 51 | Criteria = filter.Criteria; 52 | Op = filter.Op; 53 | Property = filter.Property; 54 | } 55 | } 56 | 57 | /// 58 | public class QueryFilter : IPostgrestQueryFilter 59 | { 60 | /// 61 | /// String value to be substituted for a null criterion 62 | /// 63 | public const string NullVal = "null"; 64 | 65 | /// 66 | public string? Property { get; private set; } 67 | 68 | /// 69 | public Operator Op { get; private set; } 70 | 71 | /// 72 | public object? Criteria { get; private set; } 73 | 74 | /// 75 | /// Contractor to use single value filtering. 76 | /// 77 | /// Column name 78 | /// Operation: And, Equals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, NotEqual, Is, Adjacent, Not, Like, ILike 79 | /// 80 | public QueryFilter(string property, Operator op, object? criteria) 81 | { 82 | if (criteria is DateTime dateTime) 83 | criteria = dateTime.ToString("o", CultureInfo.InvariantCulture); 84 | if (criteria is DateTimeOffset dateTimeOffset) 85 | criteria = dateTimeOffset.ToString("o", CultureInfo.InvariantCulture); 86 | 87 | switch (op) 88 | { 89 | case Operator.And: 90 | case Operator.Equals: 91 | case Operator.GreaterThan: 92 | case Operator.GreaterThanOrEqual: 93 | case Operator.LessThan: 94 | case Operator.LessThanOrEqual: 95 | case Operator.NotEqual: 96 | case Operator.Is: 97 | case Operator.Adjacent: 98 | case Operator.Not: 99 | case Operator.Like: 100 | case Operator.ILike: 101 | Property = property; 102 | Op = op; 103 | Criteria = criteria; 104 | break; 105 | case Operator.In: 106 | case Operator.Contains: 107 | case Operator.ContainedIn: 108 | case Operator.Overlap: 109 | if (criteria is IList or IDictionary) 110 | { 111 | Property = property; 112 | Op = op; 113 | Criteria = criteria; 114 | } 115 | else 116 | { 117 | throw new PostgrestException( 118 | "List or Dictionary must be used supplied as criteria with filters that accept an array of arguments.") 119 | { Reason = FailureHint.Reason.InvalidArgument }; 120 | } 121 | break; 122 | default: 123 | throw new PostgrestException("Advanced filters require a constructor with more specific arguments") 124 | { Reason = FailureHint.Reason.InvalidArgument }; 125 | } 126 | } 127 | 128 | /// 129 | /// Constructor for Full Text Search. 130 | /// 131 | /// Column Name 132 | /// Operation: FTS, PHFTS, PLFTS, WFTS 133 | /// 134 | public QueryFilter(string property, Operator op, FullTextSearchConfig fullTextSearchConfig) 135 | { 136 | switch (op) 137 | { 138 | case Operator.FTS: 139 | case Operator.PHFTS: 140 | case Operator.PLFTS: 141 | case Operator.WFTS: 142 | Property = property; 143 | Op = op; 144 | Criteria = fullTextSearchConfig; 145 | break; 146 | default: 147 | throw new PostgrestException("Constructor must be called with a full text search operator") 148 | { Reason = FailureHint.Reason.InvalidArgument }; 149 | } 150 | } 151 | 152 | /// 153 | /// Constructor for Range Queries. 154 | /// 155 | /// 156 | /// Operator: Overlap, StrictlyLeft, StrictlyRight, NotRightOf, NotLeftOf, Adjacent 157 | /// 158 | public QueryFilter(string property, Operator op, IntRange range) 159 | { 160 | switch (op) 161 | { 162 | case Operator.Overlap: 163 | case Operator.Contains: 164 | case Operator.ContainedIn: 165 | case Operator.StrictlyLeft: 166 | case Operator.StrictlyRight: 167 | case Operator.NotRightOf: 168 | case Operator.NotLeftOf: 169 | case Operator.Adjacent: 170 | Property = property; 171 | Op = op; 172 | Criteria = range; 173 | break; 174 | default: 175 | throw new PostgrestException("Constructor must be called with a filter that accepts a range") 176 | { 177 | Reason = FailureHint.Reason.InvalidArgument 178 | }; 179 | } 180 | } 181 | 182 | /// 183 | /// Constructor to enable `AND` and `OR` Queries by allowing nested QueryFilters. 184 | /// 185 | /// Operation: And, Or 186 | /// 187 | public QueryFilter(Operator op, List filters) 188 | { 189 | switch (op) 190 | { 191 | case Operator.Or: 192 | case Operator.And: 193 | Op = op; 194 | Criteria = filters; 195 | break; 196 | default: 197 | throw new PostgrestException("Constructor can only be used with `or` or `and` filters") 198 | { Reason = FailureHint.Reason.InvalidArgument }; 199 | } 200 | } 201 | 202 | /// 203 | /// Constructor to enable `NOT` functionality 204 | /// 205 | /// Operation: Not. 206 | /// 207 | public QueryFilter(Operator op, IPostgrestQueryFilter filter) 208 | { 209 | switch (op) 210 | { 211 | case Operator.Not: 212 | Op = op; 213 | Criteria = filter; 214 | break; 215 | default: 216 | throw new PostgrestException("Constructor can only be used with `not` filter") 217 | { Reason = FailureHint.Reason.InvalidArgument }; 218 | } 219 | } 220 | } 221 | 222 | /// 223 | /// Configuration Object for Full Text Search. 224 | /// API Reference: http://postgrest.org/en/v7.0.0/api.html?highlight=full%20text%20search#full-text-search 225 | /// 226 | public class FullTextSearchConfig 227 | { 228 | /// 229 | /// Query Text 230 | /// 231 | [JsonProperty("queryText")] 232 | public string QueryText { get; private set; } 233 | 234 | /// 235 | /// Defaults to english 236 | /// 237 | [JsonProperty("config")] 238 | public string Config { get; private set; } = "english"; 239 | 240 | /// 241 | /// Constructor for Full Text Search. 242 | /// 243 | /// 244 | /// 245 | public FullTextSearchConfig(string queryText, string? config) 246 | { 247 | QueryText = queryText; 248 | 249 | if (!string.IsNullOrEmpty(config)) 250 | Config = config!; 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /Postgrest/QueryOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Supabase.Postgrest.Extensions; 4 | using Supabase.Core.Attributes; 5 | #pragma warning disable CS1591 6 | namespace Supabase.Postgrest 7 | { 8 | 9 | public class QueryOptions 10 | { 11 | public enum ReturnType 12 | { 13 | [MapTo("minimal")] 14 | Minimal, 15 | [MapTo("representation")] 16 | Representation 17 | }; 18 | 19 | public enum CountType 20 | { 21 | [MapTo("none")] 22 | None, 23 | [MapTo("exact")] 24 | Exact, 25 | [MapTo("planned")] 26 | Planned, 27 | [MapTo("estimated")] 28 | Estimated 29 | }; 30 | 31 | public enum DuplicateResolutionType 32 | { 33 | [MapTo("merge-duplicates")] 34 | MergeDuplicates, 35 | [MapTo("ignore-duplicates")] 36 | IgnoreDuplicates 37 | } 38 | 39 | /// 40 | /// By default the new record is returned. Set this to 'Minimal' if you don't need this value. 41 | /// 42 | public ReturnType Returning { get; set; } = ReturnType.Representation; 43 | 44 | /// 45 | /// Specifies if duplicate rows should be ignored and not inserted. 46 | /// 47 | public DuplicateResolutionType DuplicateResolution { get; set; } = DuplicateResolutionType.MergeDuplicates; 48 | 49 | /// 50 | /// Count algorithm to use to count rows in a table. 51 | /// 52 | public CountType Count { get; set; } = CountType.None; 53 | 54 | /// 55 | /// If the record should be upserted 56 | /// 57 | public bool Upsert { get; set; } 58 | 59 | /// 60 | /// /// By specifying the onConflict query parameter, you can make UPSERT work on a column(s) that has a UNIQUE constraint. 61 | /// 62 | public string? OnConflict { get; set; } 63 | 64 | public Dictionary ToHeaders() 65 | { 66 | var headers = new Dictionary(); 67 | var prefersHeaders = new List(); 68 | 69 | if (Upsert) 70 | { 71 | var resolverAttr = DuplicateResolution.GetAttribute(); 72 | prefersHeaders.Add($"resolution={resolverAttr?.Mapping}"); 73 | } 74 | 75 | var returnAttr = Returning.GetAttribute(); 76 | if (returnAttr != null) 77 | { 78 | prefersHeaders.Add($"return={returnAttr.Mapping}"); 79 | } 80 | 81 | var countAttr = Count.GetAttribute(); 82 | if (Count != CountType.None && countAttr != null) 83 | { 84 | prefersHeaders.Add($"count={countAttr.Mapping}"); 85 | } 86 | 87 | headers.Add("Prefer", String.Join(",", prefersHeaders.ToArray())); 88 | 89 | if (Returning == ReturnType.Minimal) 90 | { 91 | headers.Add("Accept", "*/*"); 92 | } 93 | 94 | return headers; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Postgrest/QueryOrderer.cs: -------------------------------------------------------------------------------- 1 | using static Supabase.Postgrest.Constants; 2 | #pragma warning disable CS1591 3 | 4 | namespace Supabase.Postgrest 5 | { 6 | 7 | public class QueryOrderer 8 | { 9 | public string? ForeignTable { get; } 10 | public string Column { get; } 11 | public Ordering Ordering { get; } 12 | public NullPosition NullPosition { get; } 13 | 14 | public QueryOrderer(string? foreignTable, string column, Ordering ordering, NullPosition nullPosition) 15 | { 16 | ForeignTable = foreignTable; 17 | Column = column; 18 | Ordering = ordering; 19 | NullPosition = nullPosition; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Postgrest/Requests/CacheBackedRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Supabase.Postgrest.Interfaces; 8 | using Supabase.Postgrest.Models; 9 | using Supabase.Postgrest.Responses; 10 | 11 | namespace Supabase.Postgrest.Requests 12 | { 13 | /// 14 | /// Represents a Request that is backed by a caching strategy. 15 | /// 16 | /// 17 | public class CacheBackedRequest : INotifyPropertyChanged where TModel : BaseModel, new() 18 | { 19 | /// 20 | /// Handler for when Remote Models have been populated 21 | /// 22 | public delegate void RemoteModelsPopulatedEventHandler(CacheBackedRequest sender); 23 | 24 | /// 25 | /// The Async action that represents the Remote Request 26 | /// 27 | private readonly Func>> _remoteRequestAction; 28 | 29 | /// 30 | /// The Postgrest Table Instance 31 | /// 32 | private readonly IPostgrestTableWithCache _instance; 33 | 34 | /// 35 | /// The Cache lookup key - a Base64 encoded reproducible URL for this request configuration. 36 | /// 37 | private string CacheKey { get; } 38 | 39 | /// 40 | /// The Caching provider. 41 | /// 42 | private readonly IPostgrestCacheProvider _cacheProvider; 43 | 44 | private List _models = new List(); 45 | private ModeledResponse? _response; 46 | private bool _wasCacheHit; 47 | private DateTime? _cacheTime; 48 | private bool _wasResponseCached; 49 | 50 | /// 51 | /// The Models returned either by Cache Hit or Remote Response 52 | /// 53 | public List Models 54 | { 55 | get => _models; 56 | set => SetField(ref _models, value); 57 | } 58 | 59 | /// 60 | /// The response (if applicable) from 61 | /// 62 | public ModeledResponse? Response 63 | { 64 | get => _response; 65 | protected set => SetField(ref _response, value); 66 | } 67 | 68 | /// 69 | /// If the cache was hit for this request. 70 | /// 71 | public bool WasCacheHit 72 | { 73 | get => _wasCacheHit; 74 | protected set => SetField(ref _wasCacheHit, value); 75 | } 76 | 77 | /// 78 | /// If the response was stored in cache. 79 | /// 80 | public bool WasResponseCached 81 | { 82 | get => _wasResponseCached; 83 | protected set => SetField(ref _wasResponseCached, value); 84 | } 85 | 86 | /// 87 | /// The stored cache time in UTC. 88 | /// 89 | public DateTime? CacheTime 90 | { 91 | get => _cacheTime; 92 | protected set => SetField(ref _cacheTime, value); 93 | } 94 | 95 | /// 96 | public event PropertyChangedEventHandler? PropertyChanged; 97 | 98 | /// 99 | /// Invoked when Remote Models have been populated on this object. 100 | /// 101 | public event RemoteModelsPopulatedEventHandler? RemoteModelsPopulated; 102 | 103 | /// 104 | /// Constructs a Cache Backed Request that automatically populates itself using the Cache provider (if possible). 105 | /// 106 | /// 107 | /// 108 | /// 109 | public CacheBackedRequest(IPostgrestTableWithCache instance, IPostgrestCacheProvider cacheProvider, 110 | Func>> remoteRequestAction) 111 | { 112 | _instance = instance; 113 | _cacheProvider = cacheProvider; 114 | _remoteRequestAction = remoteRequestAction; 115 | 116 | CacheKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(_instance.GenerateUrl())); 117 | } 118 | 119 | /// 120 | /// Attempts to load a model from the cache. 121 | /// 122 | internal async Task TryLoadFromCache() 123 | { 124 | try 125 | { 126 | var cachedModel = await _cacheProvider.GetItem?>(CacheKey); 127 | 128 | if (cachedModel == null) return; 129 | 130 | Debugger.Instance.Log(this, $"Loaded cached model from key: {CacheKey}"); 131 | 132 | WasCacheHit = true; 133 | CacheTime = cachedModel.CachedAt; 134 | Models = cachedModel.Models ?? new List(); 135 | } 136 | catch (Exception ex) 137 | { 138 | Debugger.Instance.Log(this, ex.Message); 139 | } 140 | } 141 | 142 | /// 143 | /// Invokes the stored 144 | /// 145 | internal async void Invoke() 146 | { 147 | var result = await _remoteRequestAction.Invoke(); 148 | await Cache(result); 149 | 150 | RemoteModelsPopulated?.Invoke(this); 151 | } 152 | 153 | /// 154 | /// Caches a modeled response using the 155 | /// 156 | /// 157 | private async Task Cache(ModeledResponse response) 158 | { 159 | var cacheTime = DateTime.UtcNow; 160 | var modelToBeCached = new CachedModel 161 | { 162 | Models = response.Models, 163 | CachedAt = cacheTime 164 | }; 165 | 166 | await _cacheProvider.SetItem(CacheKey, modelToBeCached); 167 | 168 | Response = response; 169 | CacheTime = cacheTime; 170 | WasResponseCached = true; 171 | } 172 | 173 | /// 174 | /// Raises a property change event. 175 | /// 176 | /// 177 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 178 | { 179 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 180 | } 181 | 182 | /// 183 | /// Sets a field within this instance and raises 184 | /// 185 | /// 186 | /// 187 | /// 188 | /// 189 | /// 190 | private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) 191 | { 192 | if (EqualityComparer.Default.Equals(field, value)) return false; 193 | field = value; 194 | OnPropertyChanged(propertyName); 195 | return true; 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /Postgrest/Responses/BaseResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Newtonsoft.Json; 3 | #pragma warning disable CS1591 4 | namespace Supabase.Postgrest.Responses 5 | { 6 | 7 | /// 8 | /// A wrapper class from which all Responses derive. 9 | /// 10 | public class BaseResponse 11 | { 12 | [JsonIgnore] 13 | public HttpResponseMessage? ResponseMessage { get; set; } 14 | 15 | [JsonIgnore] 16 | public string? Content { get; set; } 17 | 18 | [JsonIgnore] 19 | public ClientOptions ClientOptions { get; set; } 20 | 21 | public BaseResponse(ClientOptions clientOptions, HttpResponseMessage? responseMessage, string? content) 22 | { 23 | ClientOptions = clientOptions; 24 | ResponseMessage = responseMessage; 25 | Content = content; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Postgrest/Responses/ModeledResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | using Supabase.Postgrest.Extensions; 7 | using Supabase.Postgrest.Models; 8 | 9 | namespace Supabase.Postgrest.Responses 10 | { 11 | 12 | /// 13 | /// A representation of a successful Postgrest response that transforms the string response into a C# Modelled response. 14 | /// 15 | /// 16 | public class ModeledResponse : BaseResponse where T : BaseModel, new() 17 | { 18 | /// 19 | /// The first model in the response. 20 | /// 21 | public T? Model => Models.FirstOrDefault(); 22 | 23 | /// 24 | /// A list of models in the response. 25 | /// 26 | public List Models { get; } = new(); 27 | 28 | /// 29 | /// The number of results matching the specified filters 30 | /// 31 | public int Count = 0; 32 | 33 | /// 34 | public ModeledResponse(BaseResponse baseResponse, JsonSerializerSettings serializerSettings, Func>? getHeaders = null, bool shouldParse = true) : base(baseResponse.ClientOptions, baseResponse.ResponseMessage, baseResponse.Content) 35 | { 36 | Content = baseResponse.Content; 37 | ResponseMessage = baseResponse.ResponseMessage; 38 | 39 | if (!shouldParse || string.IsNullOrEmpty(Content)) return; 40 | 41 | var token = JToken.Parse(Content!); 42 | 43 | switch (token) 44 | { 45 | // A List of models has been returned 46 | case JArray: { 47 | var deserialized = JsonConvert.DeserializeObject>(Content!, serializerSettings); 48 | 49 | if (deserialized != null) 50 | Models = deserialized; 51 | 52 | foreach (var model in Models) 53 | { 54 | model.BaseUrl = baseResponse.ResponseMessage!.RequestMessage.RequestUri.GetInstanceUrl().Replace(model.TableName, "").TrimEnd('/'); 55 | model.RequestClientOptions = ClientOptions; 56 | model.GetHeaders = getHeaders; 57 | } 58 | 59 | break; 60 | } 61 | // A single model has been returned 62 | case JObject: { 63 | Models.Clear(); 64 | 65 | var obj = JsonConvert.DeserializeObject(Content!, serializerSettings); 66 | 67 | if (obj != null) 68 | { 69 | obj.BaseUrl = baseResponse.ResponseMessage!.RequestMessage.RequestUri.GetInstanceUrl().Replace(obj.TableName, "").TrimEnd('/'); 70 | obj.RequestClientOptions = ClientOptions; 71 | obj.GetHeaders = getHeaders; 72 | 73 | Models.Add(obj); 74 | } 75 | 76 | break; 77 | } 78 | } 79 | 80 | try 81 | { 82 | var countStr = baseResponse.ResponseMessage?.Content.Headers.GetValues("Content-Range") 83 | .FirstOrDefault(); 84 | Count = int.Parse(countStr?.Split('/')[1] ?? throw new InvalidOperationException()); 85 | } 86 | catch (Exception e) 87 | { 88 | Debugger.Instance.Log(this, e.Message); 89 | Count = -1; 90 | } 91 | 92 | Debugger.Instance.Log(this, $"Response: [{baseResponse.ResponseMessage?.StatusCode}]\n" + $"Parsed Models <{typeof(T).Name}>:\n\t{JsonConvert.SerializeObject(Models)}\n"); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Postgrest/TableWithCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json; 5 | using Supabase.Postgrest.Interfaces; 6 | using Supabase.Postgrest.Models; 7 | using Supabase.Postgrest.Requests; 8 | using Supabase.Postgrest.Responses; 9 | 10 | namespace Supabase.Postgrest 11 | { 12 | /// 13 | /// Represents a table constructed with a 14 | /// 15 | /// 16 | public class TableWithCache : Table, IPostgrestTableWithCache where T : BaseModel, new() 17 | { 18 | /// 19 | /// Represents a caching provider to be used with Get Requests. 20 | /// 21 | protected IPostgrestCacheProvider CacheProvider { get; } 22 | 23 | /// 24 | public TableWithCache(string baseUrl, IPostgrestCacheProvider cacheProvider, 25 | JsonSerializerSettings serializerSettings, ClientOptions? options = null) 26 | : base(baseUrl, serializerSettings, options) 27 | { 28 | CacheProvider = cacheProvider; 29 | } 30 | 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | public new async Task> Get(CancellationToken cancellationToken = default) 37 | { 38 | var action = new Func>>(() => base.Get(cancellationToken)); 39 | 40 | var cacheModel = new CacheBackedRequest(this, CacheProvider, action); 41 | await cacheModel.TryLoadFromCache(); 42 | 43 | cacheModel.Invoke(); 44 | 45 | return cacheModel; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /PostgrestExample/Models/Message.cs: -------------------------------------------------------------------------------- 1 | using Supabase.Postgrest.Attributes; 2 | using Supabase.Postgrest.Models; 3 | 4 | namespace PostgrestExample.Models 5 | { 6 | [Table("messages")] 7 | public class Message : BaseModel 8 | { 9 | [Column("channel_id")] 10 | public int ChannelId { get; set; } 11 | 12 | [Column("message")] 13 | public string MessageData { get; set; } = null!; 14 | 15 | [Column("data")] 16 | public string Data { get; set; } = null!; 17 | 18 | [Column("username")] 19 | public string Username { get; set; } = null!; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PostgrestExample/Models/Movie.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Supabase.Postgrest.Attributes; 4 | using Supabase.Postgrest.Models; 5 | 6 | namespace PostgrestExample.Models 7 | { 8 | [Table("movie")] 9 | public class Movie : BaseModel 10 | { 11 | [PrimaryKey("id", false)] 12 | public int Id { get; set; } 13 | 14 | [Column("name")] 15 | public string Name { get; set; } = null!; 16 | 17 | [Reference(typeof(Person))] 18 | public List Persons { get; set; } = new(); 19 | 20 | 21 | [Column("created_at")] 22 | public DateTime CreatedAt { get; set; } 23 | } 24 | 25 | [Table("person")] 26 | public class Person : BaseModel 27 | { 28 | [PrimaryKey("id",false)] 29 | public int Id { get; set; } 30 | 31 | [Column("first_name")] 32 | public string FirstName { get; set; } = null!; 33 | 34 | [Column("last_name")] 35 | public string LastName { get; set; } = null!; 36 | 37 | [Reference(typeof(Profile))] 38 | public Profile Profile { get; set; } = null!; 39 | 40 | [Column("created_at")] 41 | public DateTime CreatedAt { get; set; } 42 | } 43 | 44 | [Table("profile")] 45 | public class Profile : BaseModel 46 | { 47 | [Column("email")] 48 | public string Email { get; set; } = null!; 49 | } 50 | 51 | [Table("movie_person")] 52 | public class MoviePerson : BaseModel 53 | { 54 | [PrimaryKey("id", false)] 55 | public int Id { get; set; } 56 | 57 | [PrimaryKey("movie_id", false)] 58 | public int MovieId { get; set; } 59 | 60 | [PrimaryKey("person_id", false)] 61 | public int PersonId { get; set; } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /PostgrestExample/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Supabase.Postgrest; 3 | using Supabase.Postgrest.Attributes; 4 | using Supabase.Postgrest.Models; 5 | 6 | namespace PostgrestExample.Models 7 | { 8 | [Table("users")] 9 | public class User : BaseModel 10 | { 11 | [PrimaryKey("username")] 12 | public string Username { get; set; } = null!; 13 | 14 | [Column("data")] 15 | public string Data { get; set; } = null!; 16 | 17 | [Column("age_range")] 18 | public IntRange AgeRange { get; set; } = null!; 19 | 20 | [Column("catchphrase")] 21 | public string Catchphrase { get; set; } = null!; 22 | 23 | [Column("status")] 24 | public string Status { get; set; } = null!; 25 | 26 | [Column("inserted_at")] 27 | public DateTime InsertedAt { get; set; } 28 | 29 | [Column("updated_at")] 30 | public DateTime UpdatedAt { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PostgrestExample/PostgrestExample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net8.0;net48 5 | latest 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /PostgrestExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading.Tasks; 3 | using Supabase.Postgrest; 4 | using PostgrestExample.Models; 5 | 6 | namespace PostgrestExample 7 | { 8 | class Program 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | var url = "http://localhost:3000"; 13 | var client = new Supabase.Postgrest.Client(url); 14 | 15 | // Get all Users 16 | var users = await client.Table().Get(); 17 | 18 | foreach (var user in users.Models) 19 | { 20 | Debug.WriteLine($"{user.Username} often says: {user.Catchphrase}"); 21 | } 22 | 23 | // Get a single User 24 | var supabotUser = await client.Table().Filter("username", Supabase.Postgrest.Constants.Operator.Equals, "supabot").Single(); 25 | 26 | if (supabotUser != null) 27 | { 28 | Debug.WriteLine($"{supabotUser.Username} was born on: {supabotUser.InsertedAt}"); 29 | 30 | // Use username to Query another table 31 | var supabotMessages = await client.Table().Filter("username", Supabase.Postgrest.Constants.Operator.Equals, supabotUser.Username).Get(); 32 | 33 | if (supabotMessages != null) 34 | { 35 | Debug.WriteLine("and has said..."); 36 | 37 | foreach (var message in supabotMessages.Models) 38 | { 39 | Debug.WriteLine(message.MessageData); 40 | } 41 | } 42 | } 43 | 44 | var newUser = new User 45 | { 46 | Username = "Ash Ketchum", 47 | AgeRange = new IntRange(20, 25), 48 | Catchphrase = "Gotta catch them all", 49 | Status = "ONLINE" 50 | }; 51 | 52 | var exists = await client.Table().Filter("username", Supabase.Postgrest.Constants.Operator.Equals, "Ash Ketchum").Single(); 53 | 54 | if (exists == null) 55 | { 56 | await client.Table().Insert(newUser); 57 | } 58 | else 59 | { 60 | await exists.Delete(); 61 | } 62 | 63 | return 0; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /PostgrestTests/CoercionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Supabase.Postgrest; 7 | using PostgrestTests.Models; 8 | 9 | namespace PostgrestTests; 10 | 11 | [TestClass] 12 | public class CoercionTests 13 | { 14 | private const string BaseUrl = "http://localhost:3000"; 15 | 16 | [TestMethod("Coercion: Can coerce primitive types")] 17 | public async Task CanCoercePrimitiveTypes() 18 | { 19 | // Check against already included case (inserted in `01-dummy-data.sql` 20 | var existingItem = await new Client(BaseUrl).Table().Single(); 21 | 22 | if (existingItem != null) 23 | { 24 | Assert.AreEqual(99999.0f, existingItem.FloatValue); 25 | Assert.AreEqual(99999.0d, existingItem.DoubleValue); 26 | CollectionAssert.AreEquivalent(new List { "set", "of", "strings" }, existingItem.ListOfStrings); 27 | Assert.AreEqual(DateTime.MaxValue, existingItem.DateTimePosInfinity); 28 | Assert.AreEqual(DateTime.MinValue, existingItem.DateTimeNegInfinity); 29 | CollectionAssert.AreEquivalent(new List { 10.0f, 12.0f }, existingItem.ListOfFloats); 30 | Assert.AreEqual(new IntRange(20, 50), existingItem.IntRange); 31 | } 32 | 33 | var stringValue = "test"; 34 | var intValue = 1; 35 | var floatValue = 1.1f; 36 | var doubleValue = 1.1d; 37 | var dateTimeValue = DateTime.UtcNow; 38 | var listOfStrings = new List { "test", "1", "2", "3" }; 39 | var listOfDateTime = new List 40 | { new(2021, 12, 10), new(2021, 12, 11), new(2021, 12, 12) }; 41 | var listOfInts = new List { 1, 2, 3 }; 42 | var listOfFloats = new List { 1.1f, 1.2f, 1.3f }; 43 | var intRange = new IntRange(0, 1); 44 | 45 | var model = new KitchenSink 46 | { 47 | StringValue = stringValue, 48 | IntValue = intValue, 49 | FloatValue = floatValue, 50 | DoubleValue = doubleValue, 51 | DateTimeValue = dateTimeValue, 52 | DateTimeNegInfinity = DateTime.MinValue, 53 | DateTimePosInfinity = DateTime.MaxValue, 54 | ListOfStrings = listOfStrings, 55 | ListOfDateTimes = listOfDateTime, 56 | ListOfInts = listOfInts, 57 | ListOfFloats = listOfFloats, 58 | IntRange = intRange 59 | }; 60 | 61 | var insertedModel = await new Client(BaseUrl).Table().Insert(model); 62 | var actual = insertedModel.Models.First(); 63 | 64 | Assert.AreEqual(model.StringValue, actual.StringValue); 65 | Assert.AreEqual(model.IntValue, actual.IntValue); 66 | Assert.AreEqual(model.FloatValue, actual.FloatValue); 67 | Assert.AreEqual(model.DoubleValue, actual.DoubleValue); 68 | Assert.AreEqual(model.DateTimeValue.ToString(), actual.DateTimeValue.ToString()); 69 | CollectionAssert.AreEquivalent(model.ListOfStrings, actual.ListOfStrings); 70 | CollectionAssert.AreEquivalent(model.ListOfDateTimes, actual.ListOfDateTimes); 71 | CollectionAssert.AreEquivalent(model.ListOfInts, actual.ListOfInts); 72 | CollectionAssert.AreEquivalent(model.ListOfFloats, actual.ListOfFloats); 73 | Assert.AreEqual(model.IntRange.Start, actual.IntRange!.Start); 74 | Assert.AreEqual(model.IntRange.End, actual.IntRange.End); 75 | } 76 | 77 | 78 | [TestMethod("Coercion: Can Coerce Guids")] 79 | public async Task CanCoerceGuids() 80 | { 81 | var client = new Client(BaseUrl); 82 | client.AddDebugHandler((_, message, _) => Console.WriteLine(message)); 83 | var model = new KitchenSink(); 84 | 85 | var inserted = await client.Table().Insert(model); 86 | Assert.IsNotNull(inserted.Model); 87 | Assert.IsNull(inserted.Model.Uuidv4); 88 | 89 | var guid = Guid.NewGuid(); 90 | inserted.Model.Uuidv4 = guid; 91 | 92 | var updated = await inserted.Model.Update(); 93 | Assert.IsNotNull(updated.Model); 94 | Assert.AreEqual(guid, updated.Model.Uuidv4); 95 | } 96 | } -------------------------------------------------------------------------------- /PostgrestTests/ConverterTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using Supabase.Postgrest; 3 | using Supabase.Postgrest.Converters; 4 | using Supabase.Postgrest.Exceptions; 5 | using Supabase.Postgrest.Extensions; 6 | 7 | namespace PostgrestTests 8 | { 9 | [TestClass] 10 | public class ConverterTests 11 | { 12 | [TestMethod("`intRange` should parse according to postgres docs")] 13 | public void TestIntRangeParsing() 14 | { 15 | // Test cases from 8.17.5 https://www.postgresql.org/docs/9.3/rangetypes.html 16 | var test1 = "[3,7)"; 17 | var result1 = RangeConverter.ParseIntRange(test1); 18 | Assert.AreEqual(3, result1.Start); 19 | Assert.AreEqual(6, result1.End); 20 | 21 | var test2 = "(3,7)"; 22 | var result2 = RangeConverter.ParseIntRange(test2); 23 | Assert.AreEqual(4, result2.Start); 24 | Assert.AreEqual(6, result2.End); 25 | 26 | var test3 = "[4,4]"; 27 | var result3 = RangeConverter.ParseIntRange(test3); 28 | Assert.AreEqual(4, result3.Start); 29 | Assert.AreEqual(4, result3.End); 30 | 31 | var test4 = "[4,4)"; 32 | var result4 = RangeConverter.ParseIntRange(test4); 33 | Assert.AreEqual(0, result4.Start); 34 | Assert.AreEqual(0, result4.End); 35 | 36 | } 37 | 38 | [TestMethod("`intrange` should only accept integers for parsing")] 39 | [ExpectedException(typeof(PostgrestException))] 40 | public void TestIntRangeParseInvalidFormat() 41 | { 42 | var test = "[1.2,3]"; 43 | RangeConverter.ParseIntRange(test); 44 | } 45 | 46 | [TestMethod("`Range` should serialize into a string postgres understands")] 47 | public void TestRangeToPostgresString() 48 | { 49 | var test1 = new IntRange(1, 7).ToPostgresString(); 50 | Assert.AreEqual("[1,7]", test1); 51 | 52 | var test2 = new IntRange(4, 6).ToPostgresString(); 53 | Assert.AreEqual("[4,6]", test2); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PostgrestTests/ExceptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace PostgrestTests 2 | { 3 | internal class ExceptionTests 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PostgrestTests/ExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Supabase.Postgrest.Extensions; 4 | 5 | namespace PostgrestTests 6 | { 7 | [TestClass] 8 | public class ExtensionTests 9 | { 10 | [TestMethod] 11 | public void UriExtensionGetBaseUri() 12 | { 13 | var uri1 = new Uri("https://abcdefg.supabase.io/rest/v1?query=me-big-query"); 14 | 15 | Assert.AreEqual("https://abcdefg.supabase.io/rest/v1", uri1.GetInstanceUrl()); 16 | 17 | var uri2 = new Uri("http://localhost:3000/testing/123?query=me-big-query"); 18 | 19 | Assert.AreEqual("http://localhost:3000/testing/123", uri2.GetInstanceUrl()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PostgrestTests/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Supabase.Postgrest; 4 | 5 | namespace PostgrestTests 6 | { 7 | internal static class Helpers 8 | { 9 | internal static Client GetHostedClient() 10 | { 11 | var url = Environment.GetEnvironmentVariable("SUPABASE_URL"); 12 | var publicKey = Environment.GetEnvironmentVariable("SUPABASE_PUBLIC_KEY"); 13 | 14 | var client = new Client($"{url}/rest/v1", new ClientOptions 15 | { 16 | Headers = new Dictionary 17 | { 18 | {"apikey", publicKey! } 19 | } 20 | }); 21 | 22 | return client; 23 | } 24 | 25 | internal static Client GetLocalClient() 26 | { 27 | var url = Environment.GetEnvironmentVariable("SUPABASE_URL"); 28 | if (url == null) url = "http://localhost:3000"; 29 | var publicKey = Environment.GetEnvironmentVariable("SUPABASE_PUBLIC_KEY"); 30 | if (publicKey == null) publicKey = "reallyreallyreallyreallyverysafe"; 31 | 32 | var client = new Client($"{url}/rest/v1", new ClientOptions 33 | { 34 | Headers = new Dictionary 35 | { 36 | {"apikey", publicKey! } 37 | } 38 | }); 39 | 40 | return client; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PostgrestTests/Models/ForeignKeyTestModel.cs: -------------------------------------------------------------------------------- 1 | using Supabase.Postgrest.Attributes; 2 | using Supabase.Postgrest.Models; 3 | 4 | namespace PostgrestTests.Models; 5 | 6 | [Table("foreign_key_test")] 7 | public class ForeignKeyTestModel : BaseModel 8 | { 9 | [PrimaryKey("id")] public int Id { get; set; } 10 | 11 | [Reference(typeof(Movie), foreignKey: "foreign_key_test_relation_1")] 12 | public Movie MovieFK1 { get; set; } = null!; 13 | 14 | [Reference(typeof(Movie), foreignKey: "foreign_key_test_relation_2")] 15 | public Movie MovieFK2 { get; set; } = null!; 16 | 17 | [Reference(typeof(Person))] public Person RandomPersonFK { get; set; } = null!; 18 | } -------------------------------------------------------------------------------- /PostgrestTests/Models/KitchenSink.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System; 3 | using System.Collections.Generic; 4 | using Newtonsoft.Json; 5 | using Supabase.Postgrest; 6 | using Supabase.Postgrest.Attributes; 7 | using Supabase.Postgrest.Models; 8 | 9 | namespace PostgrestTests.Models 10 | { 11 | [Table("kitchen_sink")] 12 | public class KitchenSink : BaseModel 13 | { 14 | [PrimaryKey("id")] public Guid? Id { get; set; } 15 | 16 | [Column("string_value")] public string? StringValue { get; set; } 17 | 18 | [Column("bool_value")] public bool BooleanValue { get; set; } 19 | 20 | [Column("unique_value")] public string? UniqueValue { get; set; } 21 | 22 | [Column("int_value")] public int? IntValue { get; set; } 23 | 24 | [Column("long_value")] public long? LongValue { get; set; } 25 | 26 | [Column("float_value")] public float FloatValue { get; set; } 27 | 28 | [Column("double_value")] public double DoubleValue { get; set; } 29 | 30 | [Column("datetime_value")] public DateTime? DateTimeValue { get; set; } 31 | 32 | [Column("datetime_value_1", NullValueHandling = NullValueHandling.Ignore)] 33 | public DateTime? DateTimeValue1 { get; set; } 34 | 35 | [Column("datetime_pos_infinite_value", NullValueHandling = NullValueHandling.Ignore)] 36 | public DateTime? DateTimePosInfinity { get; set; } 37 | 38 | [Column("datetime_neg_infinite_value", NullValueHandling = NullValueHandling.Ignore)] 39 | public DateTime? DateTimeNegInfinity { get; set; } 40 | 41 | [Column("list_of_strings")] public List? ListOfStrings { get; set; } 42 | 43 | [Column("list_of_datetimes", NullValueHandling = NullValueHandling.Ignore)] 44 | public List? ListOfDateTimes { get; set; } 45 | 46 | [Column("list_of_ints")] public List? ListOfInts { get; set; } 47 | 48 | [Column("list_of_floats")] public List? ListOfFloats { get; set; } 49 | 50 | [Column("int_range")] public IntRange? IntRange { get; set; } 51 | 52 | [Column("uuidv4")] public Guid? Uuidv4 { get; set; } 53 | } 54 | } -------------------------------------------------------------------------------- /PostgrestTests/Models/LinkedModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Supabase.Postgrest.Attributes; 4 | using Supabase.Postgrest.Models; 5 | 6 | namespace PostgrestTests.Models; 7 | 8 | [Table("movie")] 9 | public class Movie : BaseModel 10 | { 11 | [PrimaryKey("id")] public string Id { get; set; } = null!; 12 | 13 | [Column("name")] public string? Name { get; set; } 14 | 15 | [Column("status")] public MovieStatus? Status { get; set; } 16 | 17 | [Reference(typeof(Person), ReferenceAttribute.JoinType.Left)] 18 | public List People { get; set; } = new(); 19 | 20 | [Column("created_at")] public DateTime CreatedAt { get; set; } 21 | } 22 | 23 | public enum MovieStatus 24 | { 25 | OnDisplay, 26 | OffDisplay 27 | } 28 | 29 | [Table("person")] 30 | public class Person : BaseModel 31 | { 32 | [PrimaryKey("id")] public string Id { get; set; } = null!; 33 | 34 | [Reference(typeof(Movie))] public List Movies { get; set; } = new(); 35 | 36 | [Reference(typeof(Profile))] 37 | public Profile? Profile { get; set; } 38 | 39 | [Column("first_name")] public string? FirstName { get; set; } 40 | 41 | [Column("last_name")] public string? LastName { get; set; } 42 | 43 | [Column("created_at")] public DateTime CreatedAt { get; set; } 44 | } 45 | 46 | [Table("profile")] 47 | public class Profile : BaseModel 48 | { 49 | [PrimaryKey("person_id", true)] public string PersonId { get; set; } = null!; 50 | 51 | [Reference(typeof(Person))] public Person? Person { get; set; } 52 | [Column("email")] public string? Email { get; set; } 53 | 54 | [Column("created_at")] public DateTime CreatedAt { get; set; } 55 | } 56 | 57 | [Table("movie_person")] 58 | public class MoviePerson : BaseModel 59 | { 60 | [Column("movie_id")] public string? MovieId { get; set; } 61 | [Column("person_id")] public string? PersonId { get; set; } 62 | } -------------------------------------------------------------------------------- /PostgrestTests/Models/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Supabase.Postgrest.Attributes; 3 | using Supabase.Postgrest.Models; 4 | 5 | namespace PostgrestTests.Models 6 | { 7 | [Table("messages")] 8 | public class Message : BaseModel 9 | { 10 | [PrimaryKey("id")] 11 | public int Id { get; set; } 12 | 13 | [Column("username")] 14 | public string? UserName { get; set; } 15 | 16 | [Column("channel_id")] 17 | public int? ChannelId { get; set; } 18 | 19 | public override bool Equals(object? obj) 20 | { 21 | return obj is Message message && 22 | Id == message.Id; 23 | } 24 | 25 | public override int GetHashCode() 26 | { 27 | return HashCode.Combine(Id); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PostgrestTests/Models/NestedForeignKeyTestModel.cs: -------------------------------------------------------------------------------- 1 | using Supabase.Postgrest.Attributes; 2 | using Supabase.Postgrest.Models; 3 | 4 | namespace PostgrestTests.Models; 5 | 6 | [Table("nested_foreign_key_test")] 7 | public class NestedForeignKeyTestModel : BaseModel 8 | { 9 | [PrimaryKey("id")] public int Id { get; set; } 10 | 11 | [Reference(typeof(ForeignKeyTestModel))] 12 | public ForeignKeyTestModel FKTestModel { get; set; } = null!; 13 | 14 | [Reference(typeof(User))] public User User { get; set; } = null!; 15 | } -------------------------------------------------------------------------------- /PostgrestTests/Models/Stub.cs: -------------------------------------------------------------------------------- 1 | using Supabase.Postgrest.Models; 2 | 3 | namespace PostgrestTests.Models 4 | { 5 | public class Stub : BaseModel 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PostgrestTests/Models/Todo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using Supabase.Postgrest.Attributes; 5 | using Supabase.Postgrest.Models; 6 | 7 | namespace PostgrestTests.Models 8 | { 9 | [Table("todos")] 10 | public class Todo : BaseModel 11 | { 12 | [JsonConverter(typeof(StringEnumConverter))] 13 | public enum TodoStatus 14 | { 15 | [EnumMember(Value = "NOT STARTED")] 16 | NOT_STARTED, 17 | [EnumMember(Value = "IN PROGRESS")] 18 | IN_PROGRESS, 19 | [EnumMember(Value = "DONE")] 20 | DONE, 21 | } 22 | 23 | [PrimaryKey("id")] 24 | public int Id { get; set; } 25 | 26 | [Column("user_id")] 27 | public int UserId { get; set; } 28 | 29 | [Column("status")] 30 | public TodoStatus Status { get; set; } 31 | 32 | [Column("name")] 33 | public string? Name { get; set; } 34 | 35 | [Column("notes")] 36 | public string? Notes { get; set; } 37 | 38 | [Column("done")] 39 | public bool Done { get; set; } 40 | 41 | [Column("details")] 42 | public string? Details { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /PostgrestTests/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Supabase.Postgrest; 4 | using Supabase.Postgrest.Attributes; 5 | using Supabase.Postgrest.Models; 6 | 7 | namespace PostgrestTests.Models 8 | { 9 | [Table("users")] 10 | public class User : BaseModel 11 | { 12 | [PrimaryKey("username", true)] 13 | public string? Username { get; set; } 14 | 15 | [Column("data")] 16 | public string? Data { get; set; } 17 | 18 | [Column("favorite_numbers")] 19 | public List? FavoriteNumbers { get; set; } 20 | 21 | [Column("favorite_name")] 22 | public string? FavoriteName { get; set; } 23 | 24 | [Column("age_range")] 25 | public IntRange? AgeRange { get; set; } 26 | 27 | [Column("catchphrase")] 28 | public string? Catchphrase { get; set; } 29 | 30 | [Column("status")] 31 | public string? Status { get; set; } 32 | 33 | [Column("inserted_at")] 34 | public DateTime InsertedAt { get; set; } 35 | 36 | [Column("updated_at")] 37 | public DateTime UpdatedAt { get; set; } 38 | 39 | public override bool Equals(object? obj) 40 | { 41 | return obj is User user && 42 | Username == user.Username && 43 | Catchphrase == user.Catchphrase; 44 | } 45 | 46 | public override int GetHashCode() 47 | { 48 | return HashCode.Combine(Username, Catchphrase); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PostgrestTests/PostgrestTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | false 5 | enable 6 | latest 7 | CS8600;CS8602;CS8603 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /PostgrestTests/ReferenceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Supabase.Postgrest; 7 | using PostgrestTests.Models; 8 | using static Supabase.Postgrest.Constants; 9 | 10 | namespace PostgrestTests 11 | { 12 | [TestClass] 13 | public class ReferenceTests 14 | { 15 | private const string BaseUrl = "http://localhost:3000"; 16 | 17 | [TestMethod("Reference: Returns linked models on a root model.")] 18 | public async Task TestReferenceReturnsLinkedModels() 19 | { 20 | var client = new Client(BaseUrl); 21 | 22 | var movies = await client.Table() 23 | .Order(x => x.Id, Ordering.Ascending) 24 | .Get(); 25 | 26 | Assert.IsTrue(movies.Models.Count > 0); 27 | 28 | var first = movies.Models.First(x => x.Name!.Contains("Top Gun")); 29 | Assert.IsTrue(first.People.Count > 0); 30 | 31 | var person = first.People.First(); 32 | Assert.IsNotNull(person.Profile); 33 | 34 | var person2 = await client.Table() 35 | .Filter("first_name", Operator.Equals, "Bob") 36 | .Single(); 37 | 38 | Assert.IsNotNull(person2?.Profile); 39 | 40 | var byEmail = await client.Table() 41 | .Order(x => x.CreatedAt, Ordering.Ascending) 42 | .Filter("profile.email", Operator.Equals, "bob.saggett@supabase.io") 43 | .Single(); 44 | 45 | Assert.IsNotNull(byEmail); 46 | } 47 | 48 | [TestMethod("Reference: Can create linked records.")] 49 | public async Task TestReferenceCreateLinked() 50 | { 51 | var client = new Client(BaseUrl); 52 | 53 | var movie = new Movie { Name = "Supabase in Action" }; 54 | var movieResponse = await client.Table().Insert(movie); 55 | var movieModel = movieResponse.Model; 56 | Assert.IsNotNull(movieModel); 57 | 58 | var people = new List 59 | { 60 | new() { FirstName = "John", LastName = "Doe" }, 61 | new() { FirstName = "Jane", LastName = "Buck" } 62 | }; 63 | 64 | var peopleModels = await client.Table().Insert(people); 65 | Assert.IsTrue(peopleModels.Models.Count == 2); 66 | 67 | var profiles = new List 68 | { 69 | new() { PersonId = peopleModels.Models[0].Id, Email = "john.doe@email.com" }, 70 | new() { PersonId = peopleModels.Models[1].Id, Email = "jane.buck@email.com" }, 71 | }; 72 | var profileModels = await client.Table().Insert(profiles); 73 | Assert.IsTrue(profileModels.Models.Count == 2); 74 | 75 | var moviePeople = new List 76 | { 77 | new() { PersonId = peopleModels.Models[0].Id, MovieId = movieModel.Id }, 78 | new() { PersonId = peopleModels.Models[1].Id, MovieId = movieModel.Id } 79 | }; 80 | 81 | var moviePeopleModels = await client.Table().Insert(moviePeople); 82 | Assert.IsTrue(moviePeopleModels.Models.Count == 2); 83 | 84 | var response = await client.Table().Where(x => x.Id == movieModel.Id).Get(); 85 | var testRelations = response.Model!; 86 | Assert.IsNotNull(testRelations); 87 | Assert.IsNotNull(testRelations.People.Find(x => x.Id == peopleModels.Models[0].Id)); 88 | Assert.IsNotNull(testRelations.People.Find(x => x.Id == peopleModels.Models[1].Id)); 89 | Assert.IsNotNull(testRelations.People[0].Movies.Find(x => x.Id == movieModel.Id)); 90 | Assert.IsNotNull(testRelations.People[1].Movies.Find(x => x.Id == movieModel.Id)); 91 | Assert.AreEqual(testRelations.People[0].Profile!.PersonId, profileModels.Models[0].PersonId); 92 | Assert.AreEqual(testRelations.People[1].Profile!.PersonId, profileModels.Models[1].PersonId); 93 | 94 | // Circular references should return 1 layer of references, otherwise null. 95 | Assert.IsTrue(testRelations.People[0].Movies[0].People.Count == 0); 96 | Assert.IsNotNull(testRelations.People[0].Profile!.Person); 97 | } 98 | 99 | [TestMethod("Reference: Table can reference the same foreign table multiple times.")] 100 | public async Task TestModelCanReferenceSameForeignTableMultipleTimes() 101 | { 102 | var client = new Client(BaseUrl); 103 | 104 | var response = await client.Table().Get(); 105 | 106 | Assert.IsTrue(response.Models.Count > 0); 107 | Assert.IsInstanceOfType(response.Model!.MovieFK1, typeof(Movie)); 108 | Assert.IsInstanceOfType(response.Model!.MovieFK2, typeof(Movie)); 109 | Assert.IsInstanceOfType(response.Model!.RandomPersonFK, typeof(Person)); 110 | } 111 | 112 | [TestMethod("Reference: Table can reference a nested model with the same foreign table multiple times.")] 113 | public async Task TestModelCanReferenceNestedModelWithSameForeignTableMultipleTimes() 114 | { 115 | var client = new Client(BaseUrl); 116 | 117 | var response = await client.Table().Get(); 118 | 119 | Assert.IsTrue(response.Models.Count > 0); 120 | Assert.IsInstanceOfType(response.Model!.User, typeof(User)); 121 | Assert.IsInstanceOfType(response.Model!.FKTestModel, typeof(ForeignKeyTestModel)); 122 | } 123 | 124 | [TestMethod("Reference: Model inserts and updates (ignoring reference properties) when Reference is not null.")] 125 | public async Task TestModelInsertsAndUpdatesWhenReferenceIsSpecified() 126 | { 127 | var client = new Client(BaseUrl); 128 | 129 | var newModel = new Movie() 130 | { 131 | Name = "The Blue Eyed Samurai (Movie)", 132 | Status = MovieStatus.OffDisplay, 133 | People = 134 | [ 135 | new Person { FirstName = "Maya", LastName = "Erskine" }, 136 | new Person { FirstName = "Masi", LastName = "Oka" } 137 | ] 138 | }; 139 | 140 | var insertedModel = await client.Table().Insert(newModel); 141 | 142 | Assert.IsNotNull(insertedModel.Model); 143 | Assert.IsNotNull(insertedModel.Model.Name); 144 | Assert.AreEqual(MovieStatus.OffDisplay, insertedModel.Model.Status); 145 | 146 | insertedModel.Model.Status = MovieStatus.OnDisplay; 147 | 148 | var updatedModel = await insertedModel.Model.Update(); 149 | 150 | Assert.IsNotNull(updatedModel.Model); 151 | Assert.AreEqual(MovieStatus.OnDisplay, updatedModel.Model.Status); 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /PostgrestTests/TableWithCacheTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Newtonsoft.Json; 5 | using Supabase.Postgrest; 6 | using Supabase.Postgrest.Interfaces; 7 | using Supabase.Postgrest.Requests; 8 | using PostgrestTests.Models; 9 | 10 | namespace PostgrestTests 11 | { 12 | /// 13 | /// A sample (dumb) caching implementation that saves to a file on the filesystem. 14 | /// 15 | public class TestCacheImplementation : IPostgrestCacheProvider 16 | { 17 | private readonly string _cacheDir = Path.GetTempPath(); 18 | private string CacheFilePath => Path.Join(_cacheDir, $".postgrest-csharp.cache"); 19 | 20 | public Task GetItem(string key) 21 | { 22 | if (!Path.Exists(CacheFilePath)) 23 | return Task.FromResult(default); 24 | 25 | var text = File.ReadAllText(CacheFilePath); 26 | 27 | if (string.IsNullOrEmpty(text)) 28 | return Task.FromResult(default); 29 | 30 | return Task.FromResult(JsonConvert.DeserializeObject(text)); 31 | } 32 | 33 | public async Task SetItem(string key, object value) 34 | { 35 | if (Path.Exists(CacheFilePath)) 36 | File.Delete(CacheFilePath); 37 | 38 | await using var handle = new StreamWriter(CacheFilePath); 39 | 40 | var serialized = JsonConvert.SerializeObject(value); 41 | await handle.WriteAsync(serialized); 42 | handle.Close(); 43 | } 44 | 45 | public Task ClearItem(string key) 46 | { 47 | if (Path.Exists(CacheFilePath)) 48 | File.Delete(CacheFilePath); 49 | 50 | return Task.CompletedTask; 51 | } 52 | 53 | public Task Empty() 54 | { 55 | if (Path.Exists(CacheFilePath)) 56 | File.Delete(CacheFilePath); 57 | 58 | return Task.CompletedTask; 59 | } 60 | } 61 | 62 | [TestClass] 63 | public class TableWithCacheTests 64 | { 65 | private const string BaseUrl = "http://localhost:3000"; 66 | 67 | [TestMethod("Table: Can construct with Caching Provider and raise events.")] 68 | public async Task TestCacheWorksWithGetRequests() 69 | { 70 | var tsc1 = new TaskCompletionSource(); 71 | var tsc2 = new TaskCompletionSource(); 72 | 73 | var client = new Client(BaseUrl); 74 | var cachingProvider = new TestCacheImplementation(); 75 | await cachingProvider.Empty(); 76 | 77 | // Attempting a request that should _not_ hit a cache. 78 | var initialReq = await client.Table(cachingProvider).Get(); 79 | 80 | Assert.IsTrue(initialReq.Models.Count == 0); 81 | 82 | initialReq.PropertyChanged += (sender, args) => 83 | { 84 | if (args.PropertyName != nameof(CacheBackedRequest.WasResponseCached)) return; 85 | 86 | Assert.IsTrue(initialReq.WasResponseCached); 87 | tsc1.SetResult(true); 88 | }; 89 | 90 | initialReq.RemoteModelsPopulated += sender => 91 | { 92 | Assert.IsTrue(sender.WasResponseCached); 93 | tsc2.SetResult(true); 94 | }; 95 | 96 | await Task.WhenAll(new[] { tsc1.Task, tsc2.Task }); 97 | 98 | // Attempting a request that should hit a cache. 99 | 100 | var tsc3 = new TaskCompletionSource(); 101 | var tsc4 = new TaskCompletionSource(); 102 | var cachedResourceReq = await client.Table(cachingProvider).Get(); 103 | 104 | cachedResourceReq.PropertyChanged += (_, args) => 105 | { 106 | if (args.PropertyName != nameof(CacheBackedRequest.WasResponseCached)) return; 107 | 108 | Assert.IsTrue(cachedResourceReq.WasCacheHit); 109 | Assert.IsTrue(cachedResourceReq.WasResponseCached); 110 | Assert.IsNotNull(cachedResourceReq.Response); 111 | Assert.IsTrue(cachedResourceReq.Models.Count > 0); 112 | 113 | tsc3.SetResult(true); 114 | }; 115 | 116 | cachedResourceReq.RemoteModelsPopulated += sender => 117 | { 118 | Assert.IsTrue(sender.WasResponseCached); 119 | tsc4.SetResult(true); 120 | }; 121 | 122 | await Task.WhenAll(new[] { tsc3.Task, tsc4.Task }); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /PostgrestTests/db/00-schema.sql: -------------------------------------------------------------------------------- 1 | -- Create the Replication publication 2 | CREATE PUBLICATION supabase_realtime FOR ALL TABLES; 3 | 4 | -- Create a second schema 5 | CREATE SCHEMA personal; 6 | 7 | -- USERS 8 | CREATE TYPE public.user_status AS ENUM ('ONLINE', 'OFFLINE'); 9 | CREATE TABLE public.users 10 | ( 11 | username text primary key, 12 | inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 13 | updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 14 | favorite_numbers int[] DEFAULT null, 15 | favorite_name text UNIQUE null, 16 | data jsonb DEFAULT null, 17 | age_range int4range DEFAULT null, 18 | status user_status DEFAULT 'ONLINE'::public.user_status, 19 | catchphrase tsvector DEFAULT null 20 | ); 21 | ALTER TABLE public.users 22 | REPLICA IDENTITY FULL; -- Send "previous data" to supabase 23 | COMMENT ON COLUMN public.users.data IS 'For unstructured data and prototyping.'; 24 | 25 | -- CHANNELS 26 | CREATE TABLE public.channels 27 | ( 28 | id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 29 | inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 30 | updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 31 | data jsonb DEFAULT null, 32 | slug text 33 | ); 34 | ALTER TABLE public.users 35 | REPLICA IDENTITY FULL; -- Send "previous data" to supabase 36 | COMMENT ON COLUMN public.channels.data IS 'For unstructured data and prototyping.'; 37 | 38 | -- MESSAGES 39 | CREATE TABLE public.messages 40 | ( 41 | id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 42 | inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 43 | updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 44 | data jsonb DEFAULT null, 45 | message text, 46 | username text REFERENCES users NOT NULL, 47 | channel_id bigint REFERENCES channels NOT NULL 48 | ); 49 | ALTER TABLE public.messages 50 | REPLICA IDENTITY FULL; -- Send "previous data" to supabase 51 | COMMENT ON COLUMN public.messages.data IS 'For unstructured data and prototyping.'; 52 | 53 | create table "public"."kitchen_sink" 54 | ( 55 | "id" uuid DEFAULT gen_random_uuid() PRIMARY KEY, 56 | "string_value" varchar(255) null, 57 | "bool_value" BOOL DEFAULT false, 58 | "unique_value" varchar(255) UNIQUE, 59 | "int_value" INT null, 60 | "long_value" BIGINT null, 61 | "float_value" FLOAT null, 62 | "double_value" DOUBLE PRECISION null, 63 | "datetime_value" timestamp null, 64 | "datetime_value_1" timestamp null, 65 | "datetime_pos_infinite_value" timestamp null, 66 | "datetime_neg_infinite_value" timestamp null, 67 | "list_of_strings" TEXT[] null, 68 | "list_of_datetimes" DATE[] null, 69 | "list_of_ints" INT[] null, 70 | "list_of_floats" FLOAT[] null, 71 | "int_range" INT4RANGE null, 72 | "uuidv4" uuid null 73 | ); 74 | 75 | CREATE TABLE public.movie 76 | ( 77 | id uuid DEFAULT gen_random_uuid() PRIMARY KEY, 78 | created_at timestamp without time zone NOT NULL DEFAULT now(), 79 | name character varying(255) NULL, 80 | status character varying(255) NULL 81 | ); 82 | 83 | CREATE TABLE public.person 84 | ( 85 | id uuid DEFAULT gen_random_uuid() PRIMARY KEY, 86 | created_at timestamp without time zone NOT NULL DEFAULT now(), 87 | first_name character varying(255) NULL, 88 | last_name character varying(255) NULL 89 | ); 90 | 91 | CREATE TABLE public.profile 92 | ( 93 | person_id uuid PRIMARY KEY references person (id), 94 | email character varying(255) null, 95 | created_at timestamp without time zone NOT NULL DEFAULT now() 96 | ); 97 | 98 | CREATE TABLE public.movie_person 99 | ( 100 | movie_id uuid references movie (id), 101 | person_id uuid references person (id), 102 | primary key (movie_id, person_id) 103 | ); 104 | 105 | -- STORED FUNCTION 106 | CREATE FUNCTION public.get_status(name_param text) 107 | RETURNS user_status AS 108 | $$ 109 | SELECT status 110 | from users 111 | WHERE username = name_param; 112 | $$ LANGUAGE SQL IMMUTABLE; 113 | 114 | -- STORED FUNCTION WITH ROW PARAMETER 115 | CREATE FUNCTION public.get_data(param public.users) 116 | RETURNS public.users.data%TYPE AS 117 | $$ 118 | SELECT data 119 | from users u 120 | WHERE u.username = param.username; 121 | $$ LANGUAGE SQL IMMUTABLE; 122 | 123 | -- SECOND SCHEMA USERS 124 | CREATE TYPE personal.user_status AS ENUM ('ONLINE', 'OFFLINE'); 125 | CREATE TABLE personal.users 126 | ( 127 | username text primary key, 128 | inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 129 | updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, 130 | data jsonb DEFAULT null, 131 | age_range int4range DEFAULT null, 132 | status user_status DEFAULT 'ONLINE'::public.user_status 133 | ); 134 | 135 | -- SECOND SCHEMA STORED FUNCTION 136 | CREATE FUNCTION personal.get_status(name_param text) 137 | RETURNS user_status AS 138 | $$ 139 | SELECT status 140 | from users 141 | WHERE username = name_param; 142 | $$ LANGUAGE SQL IMMUTABLE; 143 | 144 | -- SECOND SCHEMA STORED FUNCTION WITH ROW PARAMETER 145 | CREATE FUNCTION personal.get_data(param personal.users) 146 | RETURNS personal.users.data%TYPE AS 147 | $$ 148 | SELECT data 149 | from users u 150 | WHERE u.username = param.username; 151 | $$ LANGUAGE SQL IMMUTABLE; 152 | 153 | create table public.foreign_key_test 154 | ( 155 | "id" serial primary key, 156 | "movie_fk_1" UUID null, 157 | "movie_fk_2" UUID null, 158 | "random_person_fk" UUID NULL 159 | ); 160 | 161 | ALTER TABLE "public"."foreign_key_test" 162 | ADD CONSTRAINT "foreign_key_test_relation_1" FOREIGN KEY ("movie_fk_1") REFERENCES "public"."movie" ("id") ON UPDATE CASCADE ON DELETE CASCADE; 163 | ALTER TABLE "public"."foreign_key_test" 164 | ADD CONSTRAINT "foreign_key_test_relation_2" FOREIGN KEY ("movie_fk_2") REFERENCES "public"."movie" ("id") ON UPDATE CASCADE ON DELETE CASCADE; 165 | ALTER TABLE "public"."foreign_key_test" 166 | ADD CONSTRAINT "foreign_key_random_person_fk" FOREIGN KEY ("random_person_fk") REFERENCES "public"."person" ("id") ON UPDATE CASCADE ON DELETE CASCADE; 167 | 168 | create table "public"."nested_foreign_key_test" 169 | ( 170 | "id" serial primary key, 171 | "foreign_key_test_fk" INT null, 172 | "user_fk" varchar(255) null 173 | ); 174 | 175 | ALTER TABLE "public"."nested_foreign_key_test" 176 | ADD CONSTRAINT "nested_foreign_key_test_relation_1" FOREIGN KEY ("foreign_key_test_fk") REFERENCES "public"."foreign_key_test" ("id") ON UPDATE CASCADE ON DELETE CASCADE; 177 | ALTER TABLE "public"."nested_foreign_key_test" 178 | ADD CONSTRAINT "nested_foreign_key_test_relation_2" FOREIGN KEY ("user_fk") REFERENCES "public"."users" ("username") ON UPDATE CASCADE ON DELETE CASCADE; -------------------------------------------------------------------------------- /PostgrestTests/db/01-dummy-data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO public.users (username, status, age_range, catchphrase) 2 | VALUES ('supabot', 'ONLINE', '[1,2)'::int4range, 'fat cat'::tsvector), 3 | ('kiwicopple', 'OFFLINE', '[25,35)'::int4range, 'cat bat'::tsvector), 4 | ('awailas', 'ONLINE', '[25,35)'::int4range, 'bat rat'::tsvector), 5 | ('acupofjose', 'OFFLINE', '[25,35)'::int4range, 'bat rat'::tsvector), 6 | ('dragarcia', 'ONLINE', '[20,30)'::int4range, 'rat fat'::tsvector); 7 | 8 | INSERT INTO public.channels (slug) 9 | VALUES ('public'), 10 | ('random'); 11 | 12 | INSERT INTO public.messages (message, channel_id, username) 13 | VALUES ('Hello World 👋', 1, 'supabot'), 14 | ('Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.', 15 | 2, 'supabot'); 16 | 17 | INSERT INTO personal.users (username, status, age_range) 18 | VALUES ('supabot', 'ONLINE', '[1,2)'::int4range), 19 | ('kiwicopple', 'OFFLINE', '[25,35)'::int4range), 20 | ('awailas', 'ONLINE', '[25,35)'::int4range), 21 | ('dragarcia', 'ONLINE', '[20,30)'::int4range), 22 | ('leroyjenkins', 'OFFLINE', '[20,40)'::int4range); 23 | 24 | INSERT INTO public.kitchen_sink (id, 25 | string_value, 26 | bool_value, 27 | int_value, 28 | long_value, 29 | float_value, 30 | double_value, 31 | datetime_value, 32 | datetime_value_1, 33 | datetime_pos_infinite_value, 34 | datetime_neg_infinite_value, 35 | list_of_strings, 36 | list_of_datetimes, 37 | list_of_ints, 38 | list_of_floats, 39 | int_range) 40 | 41 | VALUES ('f3ff356d-5803-43a7-b125-ba10cf10fdcd', 42 | 'Im the Kitchen Sink!', 43 | false, 44 | 99999, 45 | 2147483648, 46 | '99999.0'::float4, 47 | '99999.0'::float8, 48 | 'Tue May 24 06:30:00 2022'::timestamp, 49 | 'Tue May 20 06:00:00 2022'::timestamp, 50 | 'Infinity', 51 | '-infinity', 52 | '{"set", "of", "strings"}', 53 | '{NOW()}', 54 | '{10, 20, 30, 40}', 55 | '{10.0, 12.0}', 56 | '[20,50]'::int4range); 57 | 58 | 59 | insert into "public"."movie" ("created_at", "id", "name", "status") 60 | values ('2022-08-20 00:29:45.400188', 'ea07bd86-a507-4c68-9545-b848bfe74c90', 'Top Gun: Maverick', 'OnDisplay'); 61 | insert into "public"."movie" ("created_at", "id", "name", "status") 62 | values ('2022-08-22 00:29:45.400188', 'a972a8f6-2e23-4172-be8d-7b65470ca0f4', 'Mad Max: Fury Road', 'OnDisplay'); 63 | insert into "public"."movie" ("created_at", "id", "name", "status") 64 | values ('2022-08-28 00:29:45.400188', '42fd15b1-3bff-431d-9fa5-314289beb246', 'Guns Away', 'OffDisplay'); 65 | 66 | 67 | insert into "public"."person" ("created_at", "first_name", "id", "last_name") 68 | values ('2022-08-20 00:30:02.120528', 'Tom', 'd53072eb-5e64-4e9c-8a29-3ed07076fb2f', 'Cruise'); 69 | insert into "public"."person" ("created_at", "first_name", "id", "last_name") 70 | values ('2022-08-20 00:30:02.120528', 'Tom', 'b76776ac-75ba-424f-b5bc-6cb85c2d2bbf', 'Holland'); 71 | insert into "public"."person" ("created_at", "first_name", "id", "last_name") 72 | values ('2022-08-20 00:30:33.72443', 'Bob', '6f06c038-38e0-4a39-8aac-2c5e8597856e', 'Saggett'); 73 | insert into "public"."person" ("created_at", "first_name", "id", "last_name") 74 | values ('2022-08-20 00:30:33.72443', 'Random', 'd948ca02-c432-470e-9fe5-738269491762', 'Actor'); 75 | 76 | 77 | insert into "public"."profile" ("created_at", "email", "person_id") 78 | values ('2022-08-20 00:30:33.72443', 'tom.cruise@supabase.io', 'd53072eb-5e64-4e9c-8a29-3ed07076fb2f'); 79 | insert into "public"."profile" ("created_at", "email", "person_id") 80 | values ('2022-08-20 00:30:33.72443', 'tom.holland@supabase.io', 'b76776ac-75ba-424f-b5bc-6cb85c2d2bbf'); 81 | insert into "public"."profile" ("created_at", "email", "person_id") 82 | values ('2022-08-20 00:30:33.72443', 'bob.saggett@supabase.io', '6f06c038-38e0-4a39-8aac-2c5e8597856e'); 83 | 84 | insert into "public"."movie_person" ("movie_id", "person_id") 85 | values ('ea07bd86-a507-4c68-9545-b848bfe74c90', 'd53072eb-5e64-4e9c-8a29-3ed07076fb2f'); 86 | insert into "public"."movie_person" ("movie_id", "person_id") 87 | values ('a972a8f6-2e23-4172-be8d-7b65470ca0f4', 'b76776ac-75ba-424f-b5bc-6cb85c2d2bbf'); 88 | insert into "public"."movie_person" ("movie_id", "person_id") 89 | values ('ea07bd86-a507-4c68-9545-b848bfe74c90', '6f06c038-38e0-4a39-8aac-2c5e8597856e'); 90 | insert into "public"."movie_person" ("movie_id", "person_id") 91 | values ('42fd15b1-3bff-431d-9fa5-314289beb246', 'd948ca02-c432-470e-9fe5-738269491762'); 92 | 93 | insert into "public"."foreign_key_test" ("movie_fk_1", "movie_fk_2", "random_person_fk") 94 | values ('ea07bd86-a507-4c68-9545-b848bfe74c90', 'ea07bd86-a507-4c68-9545-b848bfe74c90', 95 | 'd53072eb-5e64-4e9c-8a29-3ed07076fb2f'); 96 | 97 | insert into "public"."nested_foreign_key_test" ("foreign_key_test_fk", "user_fk") 98 | values ('1', 'awailas'); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 |

11 | 12 | --- 13 | 14 | ## [Notice]: v4.0.0 renames this package from `postgrest-csharp` to `Supabase.Postgrest`. Which includes changing the namespace from `Postgrest` to `Supabase.Postgrest`. 15 | 16 | ## Now supporting (many) LINQ expressions! 17 | 18 | ```c# 19 | await client.Table() 20 | .Select(x => new object[] { x.Id, x.Name, x.Tags, x.ReleaseDate }) 21 | .Where(x => x.Tags.Contains("Action") || x.Tags.Contains("Adventure")) 22 | .Order(x => x.ReleaseDate, Ordering.Descending) 23 | .Get(); 24 | 25 | await client.Table() 26 | .Set(x => x.WatchedAt, DateTime.Now) 27 | .Where(x => x.Id == "11111-22222-33333-44444") 28 | // Or .Filter(x => x.Id, Operator.Equals, "11111-22222-33333-44444") 29 | .Update(); 30 | 31 | ``` 32 | 33 | --- 34 | 35 | Documentation can be found [here](https://supabase-community.github.io/postgrest-csharp/api/Postgrest.html). 36 | 37 | Postgrest-csharp is written primarily as a helper library 38 | for [supabase/supabase-csharp](https://github.com/supabase/supabase-csharp), however, it should be easy enough to use 39 | outside of the supabase ecosystem. 40 | 41 | The bulk of this library is a translation and c-sharp-ification of 42 | the [supabase/postgrest-js](https://github.com/supabase/postgrest-js) library. 43 | 44 | ## Getting Started 45 | 46 | Postgrest-csharp is _heavily_ dependent on Models deriving from `BaseModel`. To interact with the API, one must have the 47 | associated 48 | model specified. 49 | 50 | To use this library on the Supabase Hosted service but separately from the `supabase-csharp`, you'll need to specify 51 | your url and public key like so: 52 | 53 | ```c# 54 | var auth = new Supabase.Gotrue.Client(new ClientOptions 55 | { 56 | Url = "https://PROJECT_ID.supabase.co/auth/v1", 57 | Headers = new Dictionary 58 | { 59 | { "apikey", SUPABASE_PUBLIC_KEY }, 60 | { "Authorization", $"Bearer {SUPABASE_USER_TOKEN}" } 61 | } 62 | }) 63 | ``` 64 | 65 | Leverage `Table`,`PrimaryKey`, and `Column` attributes to specify names of classes/properties that are different from 66 | their C# Versions. 67 | 68 | ```c# 69 | [Table("messages")] 70 | public class Message : BaseModel 71 | { 72 | [PrimaryKey("id")] 73 | public int Id { get; set; } 74 | 75 | [Column("username")] 76 | public string UserName { get; set; } 77 | 78 | [Column("channel_id")] 79 | public int ChannelId { get; set; } 80 | 81 | public override bool Equals(object obj) 82 | { 83 | return obj is Message message && 84 | Id == message.Id; 85 | } 86 | 87 | public override int GetHashCode() 88 | { 89 | return HashCode.Combine(Id); 90 | } 91 | } 92 | ``` 93 | 94 | Utilizing the client is then just a matter of instantiating it and specifying the Model one is working with. 95 | 96 | ```c# 97 | void Initialize() 98 | { 99 | var client = new Client("http://localhost:3000"); 100 | 101 | // Get All Messages 102 | var response = await client.Table().Get(); 103 | List models = response.Models; 104 | 105 | // Insert 106 | var newMessage = new Message { UserName = "acupofjose", ChannelId = 1 }; 107 | await client.Table().Insert(); 108 | 109 | // Update 110 | var model = response.Models.First(); 111 | model.UserName = "elrhomariyounes"; 112 | await model.Update(); 113 | 114 | // Delete 115 | await response.Models.Last().Delete(); 116 | } 117 | ``` 118 | 119 | ## Foreign Keys, Join Tables, and Relationships 120 | 121 | The Postgrest server does introspection on relationships between tables and supports returning query data from 122 | tables with these included. **Foreign key constrains are required for postgrest to detect these relationships.** 123 | 124 | This library implements the attribute, `Reference` to specify on a model when a relationship should be included in a 125 | query. 126 | 127 | - [One-to-one Relationships](https://postgrest.org/en/stable/api.html#one-to-one-relationships): One-to-one 128 | relationships are detected if there’s an unique constraint on a foreign key. 129 | - [One-to-many Relationships](https://postgrest.org/en/stable/api.html#one-to-many-relationships): The inverse 130 | one-to-many relationship between two tables is detected based on the foreign key reference. 131 | - [Many-to-many Relationships](https://postgrest.org/en/stable/api.html#many-to-many-relationships): Many-to-many 132 | relationships are detected based on the join table. The join table must contain foreign keys to other two tables and 133 | they must be part of its composite key. 134 | 135 | Given the following schema: 136 | 137 | ![example schema](.github/postgrest-relationship-example.drawio.png) 138 | 139 | We can define the following models: 140 | 141 | ```c# 142 | [Table("movie")] 143 | public class Movie : BaseModel 144 | { 145 | [PrimaryKey("id")] 146 | public int Id { get; set; } 147 | 148 | [Column("name")] 149 | public string Name { get; set; } 150 | 151 | [Reference(typeof(Person))] 152 | public List Persons { get; set; } 153 | 154 | [Column("created_at")] 155 | public DateTime CreatedAt { get; set; } 156 | } 157 | 158 | [Table("person")] 159 | public class Person : BaseModel 160 | { 161 | [PrimaryKey("id")] 162 | public int Id { get; set; } 163 | 164 | [Column("first_name")] 165 | public string FirstName { get; set; } 166 | 167 | [Column("last_name")] 168 | public string LastName { get; set; } 169 | 170 | [Reference(typeof(Profile))] 171 | public Profile Profile { get; set; } 172 | 173 | [Column("created_at")] 174 | public DateTime CreatedAt { get; set; } 175 | } 176 | 177 | [Table("profile")] 178 | public class Profile : BaseModel 179 | { 180 | [Column("email")] 181 | public string Email { get; set; } 182 | } 183 | ``` 184 | 185 | **Note that each related model should inherit `BaseModel` and specify its `Table` and `Column` attributes as usual.** 186 | 187 | The `Reference` Attribute by default will include the referenced model in all GET queries on the table (this can be 188 | disabled 189 | in its constructor). 190 | 191 | As such, a query on the `Movie` model (given the above) would return something like: 192 | 193 | ```js 194 | [ 195 | { 196 | id: 1, 197 | created_at: "2022-08-20T00:29:45.400188", 198 | name: "Top Gun: Maverick", 199 | person: [ 200 | { 201 | id: 1, 202 | created_at: "2022-08-20T00:30:02.120528", 203 | first_name: "Tom", 204 | last_name: "Cruise", 205 | profile: { 206 | profile_id: 1, 207 | email: "tom.cruise@supabase.io", 208 | created_at: "2022-08-20T00:30:33.72443" 209 | } 210 | }, 211 | { 212 | id: 3, 213 | created_at: "2022-08-20T00:30:33.72443", 214 | first_name: "Bob", 215 | last_name: "Saggett", 216 | profile: { 217 | profile_id: 3, 218 | email: "bob.saggett@supabase.io", 219 | created_at: "2022-08-20T00:30:33.72443" 220 | } 221 | } 222 | ] 223 | }, 224 | // ... 225 | ] 226 | ``` 227 | 228 | ### Circular References 229 | 230 | Circular relations can be added between models, however, circular relations should only be parsed one level deep for 231 | models. For example, given the 232 | models [here](https://github.com/supabase-community/postgrest-csharp/blob/master/PostgrestTests/Models/LinkedModels.cs), 233 | a raw response would look like the following (note that the `Person` object returns the root `Movie` and 234 | the `Person->Profile` returns its root `Person` object). 235 | 236 | If desired, this can be avoided by making specific join models that do not have the circular references. 237 | 238 | ```json 239 | [ 240 | { 241 | "id": "68722a22-6a6b-4410-a955-b4eb8ca7953f", 242 | "created_at": "0001-01-01T05:51:00", 243 | "name": "Supabase in Action", 244 | "person": [ 245 | { 246 | "id": "6aa849d8-dd09-4932-bc6f-6fe3b585e87f", 247 | "first_name": "John", 248 | "last_name": "Doe", 249 | "created_at": "0001-01-01T05:51:00", 250 | "movie": [ 251 | { 252 | "id": "68722a22-6a6b-4410-a955-b4eb8ca7953f", 253 | "name": "Supabase in Action", 254 | "created_at": "0001-01-01T05:51:00" 255 | } 256 | ], 257 | "profile": { 258 | "person_id": "6aa849d8-dd09-4932-bc6f-6fe3b585e87f", 259 | "email": "john.doe@email.com", 260 | "created_at": "0001-01-01T05:51:00", 261 | "person": { 262 | "id": "6aa849d8-dd09-4932-bc6f-6fe3b585e87f", 263 | "first_name": "John", 264 | "last_name": "Doe", 265 | "created_at": "0001-01-01T05:51:00" 266 | } 267 | } 268 | }, 269 | { 270 | "id": "07abc67f-bf7d-4865-b2c0-76013dc2811f", 271 | "first_name": "Jane", 272 | "last_name": "Buck", 273 | "created_at": "0001-01-01T05:51:00", 274 | "movie": [ 275 | { 276 | "id": "68722a22-6a6b-4410-a955-b4eb8ca7953f", 277 | "name": "Supabase in Action", 278 | "created_at": "0001-01-01T05:51:00" 279 | } 280 | ], 281 | "profile": { 282 | "person_id": "07abc67f-bf7d-4865-b2c0-76013dc2811f", 283 | "email": "jane.buck@email.com", 284 | "created_at": "0001-01-01T05:51:00", 285 | "person": { 286 | "id": "07abc67f-bf7d-4865-b2c0-76013dc2811f", 287 | "first_name": "Jane", 288 | "last_name": "Buck", 289 | "created_at": "0001-01-01T05:51:00" 290 | } 291 | } 292 | } 293 | ] 294 | } 295 | ] 296 | ``` 297 | 298 | ### Top Level Filtering 299 | 300 | **By default** relations expect to be used as top level filters on a query. If following the models above, this would 301 | mean that a `Movie` with no `Person` relations on it would not return on a query **unless** the `Relation` 302 | has `useInnerJoin` set to `false`: 303 | 304 | The following model would return any movie, even if there are no `Person` models associated with it: 305 | 306 | ```c# 307 | [Table("movie")] 308 | public class Movie : BaseModel 309 | { 310 | [PrimaryKey("id")] 311 | public string Id { get; set; } 312 | 313 | [Column("name")] 314 | public string? Name { get; set; } 315 | 316 | [Reference(typeof(Person), useInnerJoin: false)] 317 | public List People { get; set; } = new(); 318 | } 319 | ``` 320 | 321 | **Further Notes**: 322 | 323 | - Postgrest _does not support nested inserts or upserts_. Relational keys on models will be ignored when attempting to 324 | insert or upsert on a root model. 325 | - The `Relation` attribute uses reflection to only select the attributes specified on the Class Model (i.e. 326 | the `Profile` model has a property only for `email`, only the property will be requested in the query). 327 | 328 | ## Status 329 | 330 | - [x] Connects to PostgREST Server 331 | - [x] Authentication 332 | - [x] Basic Query Features 333 | - [x] CRUD 334 | - [x] Single 335 | - [x] Range (to & from) 336 | - [x] Limit 337 | - [x] Limit w/ Foreign Key 338 | - [x] Offset 339 | - [x] Offset w/ Foreign Key 340 | - [x] Advanced Query Features 341 | - [x] Filters 342 | - [x] Ordering 343 | - [ ] Custom Serializers 344 | - [ ] [Postgres Range](https://www.postgresql.org/docs/9.3/rangetypes.html) 345 | - [x] `int4range`, `int8range` 346 | - [ ] `numrange` 347 | - [ ] `tsrange`, `tstzrange`, `daterange` 348 | - [x] Models 349 | - [x] `BaseModel` to derive from 350 | - [x] Coercion of data into Models 351 | - [x] Unit Testing 352 | - [x] Nuget Package and Release 353 | 354 | ## Package made possible through the efforts of: 355 | 356 | | | | 357 | |:----------------------------------------------------------------------:|:---------------------------------------------------------------------------:| 358 | | [acupofjose](https://github.com/acupofjose) | [elrhomariyounes](https://github.com/elrhomariyounes) | 359 | 360 | ## Contributing 361 | 362 | We are more than happy to have contributions! Please submit a PR. 363 | -------------------------------------------------------------------------------- /Supabase.Postgrest.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.2.32519.379 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Postgrest", "Postgrest\Postgrest.csproj", "{688A0D3C-D03C-4741-945D-409E7E65083A}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostgrestTests", "PostgrestTests\PostgrestTests.csproj", "{6E8C7612-B1CA-4312-8CC5-CFBBB1D8AAAF}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostgrestExample", "PostgrestExample\PostgrestExample.csproj", "{BD58973C-AB1D-4C60-B498-AA9A9DC83F4D}" 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FA6C62F4-8003-441A-9E53-BB05FF4E3F04}" 12 | ProjectSection(SolutionItems) = preProject 13 | CHANGELOG.md = CHANGELOG.md 14 | docker-compose.yml = docker-compose.yml 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{334B963C-FE86-46D0-8C20-C35799B62FA6}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{DAE8230C-F8CB-41E7-B765-7647A205CFAE}" 21 | ProjectSection(SolutionItems) = preProject 22 | .github\workflows\build-documentation.yaml = .github\workflows\build-documentation.yaml 23 | .github\workflows\release.yml = .github\workflows\release.yml 24 | .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml 25 | EndProjectSection 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {688A0D3C-D03C-4741-945D-409E7E65083A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {688A0D3C-D03C-4741-945D-409E7E65083A}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {688A0D3C-D03C-4741-945D-409E7E65083A}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {688A0D3C-D03C-4741-945D-409E7E65083A}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {6E8C7612-B1CA-4312-8CC5-CFBBB1D8AAAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {6E8C7612-B1CA-4312-8CC5-CFBBB1D8AAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {6E8C7612-B1CA-4312-8CC5-CFBBB1D8AAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {6E8C7612-B1CA-4312-8CC5-CFBBB1D8AAAF}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {BD58973C-AB1D-4C60-B498-AA9A9DC83F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {BD58973C-AB1D-4C60-B498-AA9A9DC83F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {BD58973C-AB1D-4C60-B498-AA9A9DC83F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {BD58973C-AB1D-4C60-B498-AA9A9DC83F4D}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {1E30C1AF-404A-49D4-82BB-94AC5515D7F4} 51 | EndGlobalSection 52 | GlobalSection(MonoDevelopProperties) = preSolution 53 | Policies = $0 54 | $0.DotNetNamingPolicy = $1 55 | $1.DirectoryNamespaceAssociation = PrefixedHierarchical 56 | $0.StandardHeader = $2 57 | $0.VersionControlPolicy = $3 58 | version = 2.0.6 59 | EndGlobalSection 60 | GlobalSection(NestedProjects) = preSolution 61 | {334B963C-FE86-46D0-8C20-C35799B62FA6} = {FA6C62F4-8003-441A-9E53-BB05FF4E3F04} 62 | {DAE8230C-F8CB-41E7-B765-7647A205CFAE} = {334B963C-FE86-46D0-8C20-C35799B62FA6} 63 | EndGlobalSection 64 | EndGlobal 65 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | rest: 4 | image: postgrest/postgrest:latest 5 | ports: 6 | - "127.0.0.1:3000:3000" 7 | environment: 8 | PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres 9 | PGRST_DB_SCHEMA: public, personal 10 | PGRST_DB_ANON_ROLE: postgres 11 | PGRST_JWT_SECRET: "reallyreallyreallyreallyverysafe" 12 | depends_on: 13 | - db 14 | db: 15 | image: postgres:14 16 | ports: 17 | - "127.0.0.1:5432:5432" 18 | volumes: 19 | - ./PostgrestTests/db:/docker-entrypoint-initdb.d/ 20 | environment: 21 | POSTGRES_DB: postgres 22 | POSTGRES_USER: postgres 23 | POSTGRES_PASSWORD: postgres 24 | POSTGRES_PORT: 5432 25 | --------------------------------------------------------------------------------