diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d87766..b2f4a54 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,12 @@ updates: schedule: interval: daily timezone: America/Chicago + groups: + XUnit: + patterns: + - "xunit*" open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5b1dcac..b2f3590 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -8,6 +8,11 @@ on: # yamllint disable-line rule:truthy # The branches below must be a subset of the branches above branches: [master] +permissions: + actions: read + contents: read + security-events: write + jobs: analyze: name: Analyze @@ -15,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -23,7 +28,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java @@ -31,7 +36,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 934b10c..dc58b05 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -11,6 +11,10 @@ on: # yamllint disable-line rule:truthy branches: - master +permissions: + contents: read + pull-requests: write + env: DOTNET_NOLOGO: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true @@ -33,12 +37,12 @@ jobs: steps: - name: Checkout Branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: config-file: global.json @@ -49,7 +53,18 @@ jobs: run: dotnet build --no-restore - name: Run tests - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" + + - name: Generate test report + uses: bibipkins/dotnet-test-reporter@v1.4.1 + if: ${{ always() }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-title: 'Test Results' + results-path: tests/FMData.Rest.Tests/**/*.trx + coverage-path: tests/FMData.Rest.Tests/**/*.cobertura.xml + coverage-type: cobertura + coverage-threshold: 70 - name: Push to MyGet env: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 3bfb7b9..b9d780d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -12,6 +12,10 @@ name: Lint Code Base on: # yamllint disable-line rule:truthy pull_request: +permissions: + contents: read + statuses: write + ############### # Set the Job # ############### @@ -30,7 +34,7 @@ jobs: # Checkout the code base # ########################## - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Full git history is needed to get a proper # list of changed files within `super-linter` @@ -40,7 +44,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: github/super-linter@v5.0.0 + uses: github/super-linter@v6 env: VALIDATE_ALL_CODEBASE: false FILTER_REGEX_EXCLUDE: README.md diff --git a/README.md b/README.md index c9d7c7d..6c58283 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ | Package | Build Status | MyGet | Nuget | |---|---|---|---| -| FMData | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml)| [![Myget](https://img.shields.io/myget/filemaker/vpre/FMData.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData) | [![NuGet](https://buildstats.info/nuget/fmdata)](https://www.nuget.org/packages/FMData/) | -| FMData.Rest | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![MyGet Pre Release](https://img.shields.io/myget/filemaker/vpre/FMData.Rest.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Rest) | [![NuGet](https://buildstats.info/nuget/fmdata.rest)](https://www.nuget.org/packages/FMData.Rest/) | -| FMData.Rest.Auth.FileMakerCloud | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![FMData.Rest.Auth.FileMakerCloud](https://img.shields.io/myget/filemaker/vpre/FMData.Rest.Auth.FileMakerCloud.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Rest.Auth.FileMakerCloud) | [![NuGet](https://buildstats.info/nuget/fmdata.rest.auth.filemakercloud)](https://www.nuget.org/packages/FMData.Rest.Auth.FileMakerCloud/) | -| FMData.Xml | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![FMData.Xml](https://img.shields.io/myget/filemaker/vpre/FMData.Xml.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Xml/) | [![NuGet](https://buildstats.info/nuget/fmdata.xml)](https://www.nuget.org/packages/FMData.Xml/) | +| FMData | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml)| [![Myget](https://img.shields.io/myget/filemaker/vpre/FMData.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData) | [![NuGet](https://img.shields.io/nuget/v/FMData?style=flat&label=NuGet)](https://www.nuget.org/packages/FMData/) | +| FMData.Rest | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![MyGet Pre Release](https://img.shields.io/myget/filemaker/vpre/FMData.Rest.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Rest) | [![NuGet](https://img.shields.io/nuget/v/fmdata.rest?style=flat&label=NuGet)](https://www.nuget.org/packages/FMData.Rest/) | +| FMData.Rest.Auth.FileMakerCloud | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![FMData.Rest.Auth.FileMakerCloud](https://img.shields.io/myget/filemaker/vpre/FMData.Rest.Auth.FileMakerCloud.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Rest.Auth.FileMakerCloud) | [![NuGet](https://img.shields.io/nuget/v/fmdata.rest.auth.filemakercloud?style=flat&label=NuGet)](https://www.nuget.org/packages/FMData.Rest.Auth.FileMakerCloud/) | +| FMData.Xml | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![FMData.Xml](https://img.shields.io/myget/filemaker/vpre/FMData.Xml.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Xml/) | [![NuGet](https://img.shields.io/nuget/v/fmdata.xml?style=flat&label=NuGet)](https://www.nuget.org/packages/FMData.Xml/) | There are plenty of ways to consume RESTful APIs from .NET, but the goal of this project is to provide a blended FileMaker-idiomatic and .NET-idiomatic experience for developers consuming data from FileMaker databases in .NET applications. @@ -26,6 +26,8 @@ If you've found a bug, please submit a bug report. If you have a feature idea, o ## Repository Information +![NuGet](https://img.shields.io/nuget/dt/FMData?style=flat&label=NuGet) + [![FMData repository/commit activity the past year](https://img.shields.io/github/commit-activity/y/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/commits/master) [![FMData issues](https://img.shields.io/github/issues/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/issues) @@ -38,12 +40,6 @@ If you've found a bug, please submit a bug report. If you have a feature idea, o [![license](https://img.shields.io/github/license/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/blob/master/LICENSE) -## Breaking changes - -> *Version 5 has breaking changes. Please [review the changes](https://github.com/fuzzzerd/fmdata/milestone/7?closed=1) prior to updating your project.* ---- -> *Version 4 has several breaking changes. Please [review the changes](https://github.com/fuzzzerd/fmdata/milestone/5?closed=1) prior to updating your project.* - ## Installation Install via `dotnet add` or nuget. Stable releases are on NuGet and CI builds are on MyGet. diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 26e18a9..0000000 --- a/_config.yml +++ /dev/null @@ -1,11 +0,0 @@ -plugins: - - jekyll-seo-tag - - jekyll-avatar -theme: jekyll-theme-cayman -title: A FileMaker Data API Client -url: https://fmdata.io -author: Nate Bross -logo: /images/color-cropped.png -twitter: - username: natebross - card: summary diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..f347510 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +_site +.sass-cache +.jekyll-cache +.jekyll-metadata +vendor \ No newline at end of file diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 0000000..26084d6 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,11 @@ +--- +permalink: /404.html +layout: default +nav_exclude: true +--- + +# 404 + +Page not found :( + +The requested page could not be found. diff --git a/CNAME b/docs/CNAME similarity index 100% rename from CNAME rename to docs/CNAME diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..4a6c51f --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,35 @@ +source "https://rubygems.org" +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +gem "github-pages", group: :jekyll_plugins + +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +# gem "github-pages", group: :jekyll_plugins +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.12" + gem "jekyll-remote-theme" +end + +# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +platforms :mingw, :x64_mingw, :mswin, :jruby do + gem "tzinfo", ">= 1", "< 3" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] + +# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem +# do not have a Java counterpart. +gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] + +gem "webrick", "~> 1.8" \ No newline at end of file diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000..7085796 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,279 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.3.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + base64 (0.2.0) + bigdecimal (3.1.6) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + colorator (1.1.0) + commonmarker (0.23.10) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) + dnsruby (1.70.0) + simpleidn (~> 0.2.1) + drb (2.2.1) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + ethon (0.16.0) + ffi (>= 1.15.0) + eventmachine (1.2.7) + execjs (2.9.1) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http + ffi (1.16.3) + forwardable-extended (2.6.0) + gemoji (4.1.0) + github-pages (231) + github-pages-health-check (= 1.18.2) + jekyll (= 3.9.5) + jekyll-avatar (= 0.8.0) + jekyll-coffeescript (= 1.2.2) + jekyll-commonmark-ghpages (= 0.4.0) + jekyll-default-layout (= 0.1.5) + jekyll-feed (= 0.17.0) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.16.1) + jekyll-include-cache (= 0.2.1) + jekyll-mentions (= 1.6.0) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.16.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.3) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.8.0) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.2.0) + jekyll-theme-cayman (= 0.2.0) + jekyll-theme-dinky (= 0.2.0) + jekyll-theme-hacker (= 0.2.0) + jekyll-theme-leap-day (= 0.2.0) + jekyll-theme-merlot (= 0.2.0) + jekyll-theme-midnight (= 0.2.0) + jekyll-theme-minimal (= 0.2.0) + jekyll-theme-modernist (= 0.2.0) + jekyll-theme-primer (= 0.6.0) + jekyll-theme-slate (= 0.2.0) + jekyll-theme-tactile (= 0.2.0) + jekyll-theme-time-machine (= 0.2.0) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.13.0) + kramdown (= 2.4.0) + kramdown-parser-gfm (= 1.1.0) + liquid (= 4.0.4) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.13.6, < 2.0) + rouge (= 3.30.0) + terminal-table (~> 1.4) + github-pages-health-check (1.18.2) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (>= 4, < 8) + public_suffix (>= 3.0, < 6.0) + typhoeus (~> 1.3) + html-pipeline (2.14.3) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.8.0) + i18n (1.14.3) + concurrent-ruby (~> 1.0) + racc (~> 1.7) + jekyll (3.9.5) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (>= 0.7, < 2) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (>= 1.17, < 3) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-avatar (0.8.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.2.2) + coffee-script (~> 2.2) + coffee-script-source (~> 1.12) + jekyll-commonmark (1.4.0) + commonmarker (~> 0.22) + jekyll-commonmark-ghpages (0.4.0) + commonmarker (~> 0.23.7) + jekyll (~> 3.9.0) + jekyll-commonmark (~> 1.4.0) + rouge (>= 2.0, < 5.0) + jekyll-default-layout (0.1.5) + jekyll (>= 3.0, < 5.0) + jekyll-feed (0.17.0) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.16.1) + jekyll (>= 3.4, < 5.0) + octokit (>= 4, < 7, != 4.4.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-mentions (1.6.0) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.6.0) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + jemoji (0.13.0) + gemoji (>= 3, < 5) + html-pipeline (~> 2.2) + jekyll (>= 3.0, < 5.0) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.3.6) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.22.2) + mutex_m (0.2.0) + net-http (0.4.1) + uri + nokogiri (1.16.5-x86_64-linux) + racc (~> 1.4) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (5.0.4) + racc (1.7.3) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.3.9) + rouge (3.30.0) + rubyzip (2.3.2) + safe_yaml (1.0.5) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + simpleidn (0.2.1) + unf (~> 0.1.4) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.9.1) + unicode-display_width (1.8.0) + uri (0.13.0) + webrick (1.8.2) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + github-pages + http_parser.rb (~> 0.6.0) + jekyll-feed (~> 0.12) + jekyll-remote-theme + tzinfo (>= 1, < 3) + tzinfo-data + wdm (~> 0.1.1) + webrick (~> 1.8) + +BUNDLED WITH + 2.4.19 diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..b250101 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,23 @@ +title: A FileMaker Data API Client +# >- this means to ignore newlines until "baseurl:" +description: >- + A simple and flexible FileMaker Data API Client for .NET. Use FMData to integrate your FileMaker data into .NET websites and applications. +# the subpath of your site, e.g. /blog +baseurl: "" +# the base hostname & protocol for your site, e.g. https://fmdata.io +url: https://fmdata.io + +github_username: fuzzzerd + +logo: /images/color-cropped.png + +twitter: + username: natebross + card: summary + +# Build settings +remote_theme: just-the-docs/just-the-docs +plugins: + - jekyll-feed + - jekyll-seo-tag + - jekyll-avatar diff --git a/docs/example-use.md b/docs/example-use.md new file mode 100644 index 0000000..1a8e03c --- /dev/null +++ b/docs/example-use.md @@ -0,0 +1,177 @@ +--- +layout: default +nav_order: 3 +title: Example Use +--- + +## Example Usage + +The recommended way to consume this library is using a strongly typed model as follows. + +Please review the /tests/FMData.Rest.Tests/ project folder for expected usage flows. + +### Setting up your model + +A model should roughly match a table in your solution. Its accessed via layout. + +```csharp +// use the DataContract attribute to link your model to a layout +[DataContract(Name="NameOfYourLayout")] +public class Model +{ + [DataMember] + public string Name { get; set; } + + // if your model name does not match use DataMember + [DataMember(Name="overrideFieldName")] // the internal database field to use + public string Address { get; set; } + + [DataMember] + public string SomeContainerField { get; set; } + + // use the ContainerDataFor attribute to map container data to a byte[] + [ContainerDataFor("SomeContainerField")] // use the name in your C# model + public byte[] DataForSomeContainerField { get; set; } + + // if your model has properties you don't want mapped use + [IgnoreDataMember] // to skip mapping of the field + public string NotNeededField { get; set; } +} +``` + +### Using IHttpClientFactory + +Constructors take an `HttpClient` and you can setup the DI pipeline in Startup.cs like so for standard use: + +```csharp +services.AddSingleton(ci => new FMData.ConnectionInfo +{ + FmsUri = "https://example.com", + Username = "user", + Password = "password", + Database = "FILE_NAME" +}); +services.AddHttpClient(); +``` + +If you prefer to use a singleton instance of `IFileMakerApiClient` you have to do a little bit more work in startup. This can improve performance if you're making lots of hits to the Data API over a single request to your application: + +```csharp +services.AddHttpClient(); // setup IHttpClientFactory in the DI container +services.AddSingleton(ci => new FMData.ConnectionInfo +{ + FmsUri = "https://example.com", + Username = "user", + Password = "password", + Database = "FILE_NAME" +}); +// Keep the FileMaker client as a singleton for speed +services.AddSingleton(s => { + var hcf = s.GetRequiredService(); + var ci = s.GetRequiredService(); + return new FileMakerRestClient(hcf.CreateClient(), ci); +}); +``` + +Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/xml) and reused throughout. This is useful to manage the lifetime of `IFileMakerApiClient` as a singleton, since it stores data about FileMaker Data API tokens and reuses them as much as possible. Simply using `services.AddHttpClient();` keeps the lifetime of our similar to that of a 'managed `HttpClient`' which works for simple scenarios. + +Test both approaches in your solution and use what works. + +### Authentication with FileMaker Cloud + +We can use the `FileMakerRestClient`, when the setup is done. Just create a new `ConnectionInfo` object and set the required properties: + +```cs +var conn = new ConnectionInfo(); +conn.FmsUri = "https://{NAME}.account.filemaker-cloud.com"; +conn.Username = "user@domain.com"; +conn.Password = "********"; +conn.Database = "Reporting"; +``` + +Then instantiate the `FileMakerRestClient` with a `FileMakerCloudAuthTokenProvider` as follows: + +```cs +var fm = new FileMakerRestClient(new HttpClient(), new FileMakerCloudAuthTokenProvider(conn)); +``` + +For a full description of using FileMaker Data API with FileMaker Cloud, [see this comment](https://github.com/fuzzzerd/fmdata/issues/217#issuecomment-1203202293). + +### Performing a Find + +```csharp +var client = new FileMakerRestClient("server", "fileName", "user", "pass"); // without .fmp12 +var toFind = new Model { Name = "someName" }; +var results = await client.FindAsync(toFind); +// results = IEnumerable matching with Name field matching "someName" as a FileMaker FindRequest. +``` + +### Create a new record + +```csharp +var client = new FileMakerRestClient("server", "fileName", "user", "pass"); // without .fmp12 +var toCreate = new Model { Name = "someName", Address = "123 Main Street" }; +var results = await client.CreateAsync(toCreate); +// results is an ICreateResponse which indicates success (0/OK or Failure with FMS code/message) +``` + +### Updating a record + +```csharp +var client = new FileMakerRestClient("server", "fileName", "user", "pass"); // without .fmp12 +var fileMakerRecordId = 1; // this is the value from the calculation: Get(RecordID) +var toUpdate = new Model { Name = "someName", Address = "123 Main Street" }; +var results = await client.EditAsync(fileMakerRecordId, toCreate); +// results is an IEditResponse which indicates success (0/OK or Failure with FMS code/message) +``` + +### Find with FileMaker ID Mapping + +Note you need to add an int property to the Model `public int FileMakerRecordId { get; set; }` and provide the Func to the `FindAsync` method to tell FMData how to map the FileMaker ID returned from the API to your model. + +```csharp +Func FMRecordIdMapper = (o, id) => o.FileMakerRecordId = id; +var client = new FileMakerRestClient("server", "fileName", "user", "pass"); // without .fmp12 +var toFind = new Model { Name = "someName" }; +var results = await client.FindAsync(toFind, FMRecordIdMapper); +// results is IEnumerable matching with Name field matching "someName" as a FileMaker FindRequest. +``` + +### Find with Data Info + +```csharp +var toFind = new Model { Name = "someName" }; +var req = new FindRequest() { Layout = layout }; +req.AddQuery(toFind, false); +var (data, info) = await fdc.SendAsync(req, true); +``` + +Alternatively, if you create a calculated field `Get(RecordID)` and put it on your layout then map it the normal way. + +### Find and load Container Data + +Make sure you use the `[ContainerDataFor("NameOfContainer")]` attribute along with a `byte[]` property for processing of your model. + +```csharp +var client = new FileMakerRestClient("server", "fileName", "user", "pass"); // without .fmp12 +var toFind = new Model { Name = "someName" }; +var results = await client.FindAsync(toFind); +await client.ProcessContainers(results); +// results = IEnumerable matching with Name field matching "someName" as a FileMaker FindRequest. +``` + +### Insert or Update Container Data + +```csharp +// assume recordId = a FileMaker RecordId mapped using FMIdMapper +// assume containerDataByteArray is a byte array with file contents of some sort +var client = new FileMakerRestClient("server", "fileName", "user", "pass"); // without .fmp12 +_client.UpdateContainerAsync( + "layout", + recordId, + "containerFieldName", + "filename.jpg/png/pdf/etc", + containerDataByteArray); +``` + +> *Note: In order to create a record with container data two calls must be made. One that creates the actual record ( see above) and one that updates the container field contents.* diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..89a6da7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,51 @@ +--- +layout: default +nav_order: 1 +title: Welcome +--- + +[![fmdata logo, a C# client for FileMaker](https://raw.githubusercontent.com/fuzzzerd/fmdata/master/images/color-cropped.png)](https://github.com/fuzzzerd/fmdata) + +## Packages + +| Package | Build Status | MyGet | Nuget | +|---|---|---|---| +| FMData | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml)| [![Myget](https://img.shields.io/myget/filemaker/vpre/FMData.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData) | [![NuGet](https://buildstats.info/nuget/fmdata)](https://www.nuget.org/packages/FMData/) | +| FMData.Rest | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![MyGet Pre Release](https://img.shields.io/myget/filemaker/vpre/FMData.Rest.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Rest) | [![NuGet](https://buildstats.info/nuget/fmdata.rest)](https://www.nuget.org/packages/FMData.Rest/) | +| FMData.Rest.Auth.FileMakerCloud | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![FMData.Rest.Auth.FileMakerCloud](https://img.shields.io/myget/filemaker/vpre/FMData.Rest.Auth.FileMakerCloud.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Rest.Auth.FileMakerCloud) | [![NuGet](https://buildstats.info/nuget/fmdata.rest.auth.filemakercloud)](https://www.nuget.org/packages/FMData.Rest.Auth.FileMakerCloud/) | +| FMData.Xml | [![.NET CI Build](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml/badge.svg)](https://github.com/fuzzzerd/fmdata/actions/workflows/dotnet.yml) | [![FMData.Xml](https://img.shields.io/myget/filemaker/vpre/FMData.Xml.svg)](https://www.myget.org/feed/filemaker/package/nuget/FMData.Xml/) | [![NuGet](https://buildstats.info/nuget/fmdata.xml)](https://www.nuget.org/packages/FMData.Xml/) | + +There are plenty of ways to consume RESTful APIs from .NET, but the goal of this project is to provide a blended FileMaker-idiomatic and .NET-idiomatic experience for developers consuming data from FileMaker databases in .NET applications. + +The project is organized as three main packages, with a child Auth package for FileMaker Cloud: + +- `FMData` is the core and it contains the base and abstract classes utilized by the other implementations. +- `FMData.Rest` is for the Data API and + - `FMData.Rest.Auth.FileMakerCloud` is used for authentication to the Data API hosted by FileMaker Cloud +- `FMData.Xml` is for consuming the legacy Xml/CWP API. + +> *Note: Xml support is experimental, if you need full cwp/xml coverage [check out fmDotNet](https://github.com/fuzzzerd/fmdotnet).* + +If you've found a bug, please submit a bug report. If you have a feature idea, open an issue and consider creating a pull request. + +## Repository Information + +[![FMData repository/commit activity the past year](https://img.shields.io/github/commit-activity/y/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/commits/master) + +[![FMData issues](https://img.shields.io/github/issues/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/issues) + +[![CodeFactor](https://www.codefactor.io/repository/github/fuzzzerd/fmdata/badge)](https://www.codefactor.io/repository/github/fuzzzerd/fmdata) + +[![Code size in bytes](https://img.shields.io/github/languages/code-size/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/commits/master) + +[![Language Count](https://img.shields.io/github/languages/count/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/commits/master) + +[![license](https://img.shields.io/github/license/fuzzzerd/fmdata.svg)](https://github.com/fuzzzerd/fmdata/blob/master/LICENSE) + +## Versioning + +We use [Semantic Versioning](http://semver.org/). Using the Major.Minor.Patch syntax, we attempt to follow the basic rules + + 1. MAJOR version when you make incompatible API changes, + 2. MINOR version when you add functionality in a backwards-compatible manner, and + 3. PATCH version when you make backwards-compatible bugfixes. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..4a24b38 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,13 @@ +--- +layout: default +nav_order: 2 +title: Installation +--- + +## Installation + +Install via `dotnet add` or nuget. Stable releases are on NuGet and CI builds are on MyGet. + +```ps +dotnet add package FMData.Rest +``` diff --git a/docs/resources.md b/docs/resources.md new file mode 100644 index 0000000..3dea2df --- /dev/null +++ b/docs/resources.md @@ -0,0 +1,21 @@ +--- +layout: default +nav_order: 4 +title: Resources +--- + +## FileMaker Documentation + +Latest Versions + +- [FileMaker Data API Documentation (FMS19)](https://help.claris.com/en/data-api-guide/) +- [FileMaker Server 19 Custom Web Publishing Guide](https://help.claris.com/en/server-custom-web-publishing-guide.pdf) + +Older Versions + +- [FileMaker Data API Documentation (FMS18)](https://fmhelp.filemaker.com/docs/18/en/dataapi/) +- [FileMaker Server 18 Custom Web Publishing Guide](https://fmhelp.filemaker.com/docs/18/en/fms18_cwp_guide.pdf) +- [FileMaker Data API Documentation (FMS17)](https://fmhelp.filemaker.com/docs/17/en/dataapi/) +- [FileMaker REST API Documentation (FMS16)](https://fmhelp.filemaker.com/docs/16/en/restapi/) *-- Not Supported by this project.* +- [FileMaker Server 16 Web Publishing Guide](https://fmhelp.filemaker.com/docs/16/en/fms16_cwp_guide.pdf) +- [FileMaker Server 15 Web Publishing Guide](https://fmhelp.filemaker.com/docs/15/en/fms15_cwp_guide.pdf) diff --git a/global.json b/global.json index 023ff92..a93054a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.203", + "version": "8.0.101", "rollForward": "latestMinor" } } \ No newline at end of file diff --git a/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj b/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj index 26db41a..10bdbaf 100644 --- a/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj +++ b/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net6.0 + netstandard2.0;net6.0;net8.0 true FMData.Rest.Auth.FileMakerCloud FMData.Rest.Auth.FileMakerCloud @@ -27,7 +27,7 @@ - 5.1 + 5.2 v beta.0 @@ -48,9 +48,9 @@ - - - - + + + + diff --git a/src/FMData.Rest/FMData.Rest.csproj b/src/FMData.Rest/FMData.Rest.csproj index 616a81f..f39f103 100644 --- a/src/FMData.Rest/FMData.Rest.csproj +++ b/src/FMData.Rest/FMData.Rest.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard1.3;net45;net6.0 + netstandard2.0;netstandard1.3;net45;net6.0;net8.0 true FMData.Rest FMData.Rest @@ -27,7 +27,7 @@ - 5.1 + 5.2 v beta.0 diff --git a/src/FMData.Rest/FileMakerRestClient.cs b/src/FMData.Rest/FileMakerRestClient.cs index d0a4121..c61ab58 100644 --- a/src/FMData.Rest/FileMakerRestClient.cs +++ b/src/FMData.Rest/FileMakerRestClient.cs @@ -52,6 +52,7 @@ public class FileMakerRestClient : FileMakerApiClientBase, IFileMakerRestClient private readonly IAuthTokenProvider _authTokenProvider; private readonly bool _useNewClientForContainers = false; + private readonly string _targetVersion = "v1"; #region Constructors /// @@ -98,15 +99,32 @@ public FileMakerRestClient( { _authTokenProvider = authTokenProvider; _useNewClientForContainers = useNewClientForContainers; + switch (_authTokenProvider.ConnectionInfo?.RestTargetVersion) + { + case RestTargetVersion.v1: + _targetVersion = "v1"; + break; + case RestTargetVersion.v2: + _targetVersion = "v2"; + break; + case RestTargetVersion.vLatest: + _targetVersion = "vLatest"; + break; + default: + _targetVersion = "v1"; + break; + } + #if NETSTANDARD1_3 - var header = new System.Net.Http.Headers.ProductHeaderValue("FMData.Rest", "4"); - var userAgent = new System.Net.Http.Headers.ProductInfoHeaderValue(header); + var assembly = typeof(FileMakerRestClient).GetTypeInfo().Assembly; + var version = assembly.GetCustomAttribute()?.InformationalVersion ?? "5"; #else var assembly = Assembly.GetExecutingAssembly(); var version = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion; - var header = new System.Net.Http.Headers.ProductHeaderValue(assembly.GetName().Name, version); - var userAgent = new System.Net.Http.Headers.ProductInfoHeaderValue(header); #endif + var header = new ProductHeaderValue(assembly.GetName().Name, version); + var userAgent = new ProductInfoHeaderValue(header); + Client.DefaultRequestHeaders.UserAgent.Add(userAgent); } @@ -116,7 +134,7 @@ public FileMakerRestClient( /// /// Note we assume _fmsUri has no trailing slash as its cut off in the constructor. /// - private string BaseEndPoint => $"{FmsUri}/fmi/data/v1/databases/{FileName}"; + private string BaseEndPoint => $"{FmsUri}/fmi/data/{_targetVersion}/databases/{FileName}"; /// /// Generate the appropriate Authentication endpoint uri for this instance of the data client. @@ -268,16 +286,11 @@ public async Task LogoutAsync() #endregion #region Special Implementations - /// - /// General purpose Find Request method. Supports additional syntaxes like the { "omit" : "true" } operation. - /// This method returns a strongly typed but accepts a the more flexible request parameters. - /// - /// the type of response objects to return. - /// The layout to perform the find request on. - /// The find request dictionary. - /// An matching the request parameters. - /// Can't be a relay method, since we have to process the data specially to get our output - public override async Task> FindAsync(string layout, Dictionary req) + /// + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] + public override async Task> FindAsync( + string layout, + Dictionary req) { if (string.IsNullOrEmpty(layout)) throw new ArgumentException("Layout is required on the request."); @@ -297,6 +310,7 @@ public override async Task> FindAsync(string layout, Dictionar try { var responseJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseObject = JsonConvert.DeserializeObject>(responseJson); return responseObject.Response.Data.Select(d => d.FieldData); @@ -313,6 +327,7 @@ public override async Task> FindAsync(string layout, Dictionar /// /// The find request field/value dictionary to pass into FileMaker server. /// A wrapped in a FindResponse containing both record data and portal data. + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] public override async Task>> SendAsync(IFindRequest> req) { if (string.IsNullOrEmpty(req.Layout)) throw new ArgumentException("Layout is required on the request."); @@ -545,20 +560,11 @@ public override async Task SendAsync(IDeleteRequest req) } } - /// - /// Strongly typed find request. - /// - /// The type of response objects to return. - /// The find request parameters. - /// Function to assign the FileMaker RecordId to each instance of {T}. - /// Function to assign the FileMaker ModId to each instance of {T}. - /// Indicates whether the data information portion should be parsed. - /// An matching the request parameters. - public override async Task<(IEnumerable, DataInfoModel)> SendAsync( - IFindRequest req, - bool includeDataInfo, - Func fmId = null, - Func modId = null) + /// + public override async Task<(IEnumerable, DataInfoModel)> SendFindRequestAsync( + IFindRequest req, + Func fmId = null, + Func modId = null) { if (string.IsNullOrEmpty(req.Layout)) throw new ArgumentException("Layout is required on the find request."); @@ -591,7 +597,7 @@ public override async Task SendAsync(IDeleteRequest req) } // serialize JSON results into .NET objects - IList searchResults = new List(); + IList searchResults = new List(); foreach (var result in results) { var searchResult = ConvertJTokenToInstance(fmId, modId, result); @@ -619,7 +625,7 @@ public override async Task SendAsync(IDeleteRequest req) if (responseObject.Messages.Any(m => m.Code == "401")) { // FileMaker no records match the find request => empty list. - return (new List(), new DataInfoModel()); + return (new List(), new DataInfoModel()); } // throw FMDataException for anything not a 401. throw new FMDataException( @@ -631,7 +637,7 @@ public override async Task SendAsync(IDeleteRequest req) // not found, so return empty list if (response.StatusCode == HttpStatusCode.NotFound) { - return (new List(), new DataInfoModel()); + return (new List(), new DataInfoModel()); } // other error @@ -651,7 +657,7 @@ public override async Task RunScriptAsync(string layout, string script, await UpdateTokenDateAsync().ConfigureAwait(false); // we're about to use the token so update date used // generate request url - var uri = $"{FmsUri}/fmi/data/v1" + var uri = $"{FmsUri}/fmi/data/{_targetVersion}" + $"/databases/{Uri.EscapeDataString(FileName)}" + $"/layouts/{Uri.EscapeDataString(layout)}" + $"/script/{Uri.EscapeDataString(script)}"; @@ -847,7 +853,7 @@ public override async Task SetGlobalFieldAsync(string baseTable, stri public override async Task GetProductInformationAsync() { // generate request url - var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{FmsUri}/fmi/data/v1/productinfo"); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{FmsUri}/fmi/data/{_targetVersion}/productinfo"); // run the patch action var response = await Client.SendAsync(requestMessage).ConfigureAwait(false); @@ -881,7 +887,7 @@ public override async Task> GetDatabasesAsync() // don't need to refresh the token, because this is a basic authentication request // generate request url - var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{FmsUri}/fmi/data/v1/databases"); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{FmsUri}/fmi/data/{_targetVersion}/databases"); // special non-token auth to list databases requestMessage.Headers.Authorization = await _authTokenProvider.GetAuthenticationHeaderValue().ConfigureAwait(false); @@ -918,7 +924,7 @@ public override async Task> GetLayoutsAsync( await UpdateTokenDateAsync().ConfigureAwait(false); // we're about to use the token so update date used // generate request url - var uri = $"{FmsUri}/fmi/data/v1/" + var uri = $"{FmsUri}/fmi/data/{_targetVersion}/" + $"databases/{Uri.EscapeDataString(FileName)}/layouts"; var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); @@ -957,7 +963,7 @@ public override async Task> GetScriptsAsync( await UpdateTokenDateAsync().ConfigureAwait(false); // we're about to use the token so update date used // generate request url - var uri = $"{FmsUri}/fmi/data/v1" + var uri = $"{FmsUri}/fmi/data/{_targetVersion}" + $"/databases/{Uri.EscapeDataString(FileName)}/scripts"; var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); @@ -998,7 +1004,7 @@ public override async Task GetLayoutAsync(string layout, int? re await UpdateTokenDateAsync().ConfigureAwait(false); // we're about to use the token so update date used // generate request url - var uri = $"{FmsUri}/fmi/data/v1" + var uri = $"{FmsUri}/fmi/data/{_targetVersion}" + $"/databases/{Uri.EscapeDataString(FileName)}" + $"/layouts/{Uri.EscapeDataString(layout)}"; if (recordId.HasValue) diff --git a/src/FMData.Xml/FMData.Xml.csproj b/src/FMData.Xml/FMData.Xml.csproj index 717e515..41dd289 100644 --- a/src/FMData.Xml/FMData.Xml.csproj +++ b/src/FMData.Xml/FMData.Xml.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard1.3;net45;net6.0 + netstandard2.0;netstandard1.3;net45;net6.0;net8.0 true FMData.Xml FMData.Xml @@ -27,7 +27,7 @@ - 5.1 + 5.2 v beta.0 diff --git a/src/FMData.Xml/FileMakerXmlClient.cs b/src/FMData.Xml/FileMakerXmlClient.cs index f069823..fb124d7 100644 --- a/src/FMData.Xml/FileMakerXmlClient.cs +++ b/src/FMData.Xml/FileMakerXmlClient.cs @@ -100,14 +100,17 @@ public override Task GetByFileMakerIdAsync(string layout, int fileMakerId, /// /// Find a record using a dictionary of input parameters. /// + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] public override Task>> SendAsync(IFindRequest> req) { throw new NotImplementedException(); } - /// - /// Finds records using a layout and a dictionary of strings as criteria. - /// - public override Task> FindAsync(string layout, Dictionary req) + + /// + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] + public override Task> FindAsync( + string layout, + Dictionary req) { throw new NotImplementedException(); } @@ -177,20 +180,11 @@ public override async Task SendAsync(IEditRequest req) throw new Exception("Unable to complete request"); } - /// - /// Executes a Find Request and returns the matching objects projected by the type parameter. - /// - /// The type to project the results against. - /// The Find Request Command. - /// Return the data info portion of the request. - /// The function to map FileMaker Record Ids to an instance of T. - /// The function to map FileMaker modId to an instance of T - /// The projected results matching the find request. - public override async Task<(IEnumerable, DataInfoModel)> SendAsync( - IFindRequest req, - bool includeDataInfo, - Func fmId = null, - Func modId = null) + /// + public override async Task<(IEnumerable, DataInfoModel)> SendFindRequestAsync( + IFindRequest req, + Func fmId = null, + Func modId = null) { var response = await ExecuteRequestAsync(req).ConfigureAwait(false); @@ -225,11 +219,11 @@ public override async Task SendAsync(IEditRequest req) var records = xDocument .Descendants(_ns + "resultset") .Elements(_ns + "record") - .Select(r => new RecordBase>>> + .Select(r => new RecordBase>>> { RecordId = Convert.ToInt32(r.Attribute("record-id").Value), ModId = Convert.ToInt32(r.Attribute("mod-id").Value), - FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject(), + FieldData = FieldDataToDictionary(metadata, r.Elements(_ns + "field")).ToObject(), PortalData = r.Elements(_ns + "relatedset") .ToDictionary( k => k.Attribute("table").Value, @@ -250,8 +244,8 @@ public override async Task SendAsync(IEditRequest req) fmId?.Invoke(record.FieldData, record.RecordId); modId?.Invoke(record.FieldData, record.ModId); - // TODO: update each record's FieldData instance with the contents of its PortalData - var portals = typeof(T).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute() != null); + // update each record's FieldData instance with the contents of its PortalData + var portals = typeof(TResponse).GetTypeInfo().DeclaredProperties.Where(p => p.GetCustomAttribute() != null); foreach (var portal in portals) { var portalDataAttr = portal.GetCustomAttribute(); @@ -314,7 +308,6 @@ public override Task SetGlobalFieldAsync(string baseTable, string fie /// /// Upload data to a container field. - /// TODO: Workaround with B64 encoding and container auto-enter? /// public override Task UpdateContainerAsync(string layout, int recordId, string fieldName, string fileName, int repetition, byte[] content) { @@ -395,19 +388,13 @@ public override Task GetLayoutAsync(string layout, int? recordId throw new NotImplementedException(); } - /// - /// - /// - /// + /// public override Task> GetLayoutsAsync() { throw new NotImplementedException(); } - /// - /// - /// - /// + /// public override Task> GetScriptsAsync() { throw new NotImplementedException(); diff --git a/src/FMData/ConnectionInfo.cs b/src/FMData/ConnectionInfo.cs index 856c204..8f08e5e 100644 --- a/src/FMData/ConnectionInfo.cs +++ b/src/FMData/ConnectionInfo.cs @@ -21,6 +21,10 @@ public class ConnectionInfo /// Password to use when making the connection. /// public string Password { get; set; } + /// + /// Gets or sets the to use. + /// + public RestTargetVersion? RestTargetVersion { get; set; } #region FileMaker Cloud diff --git a/src/FMData/FMData.csproj b/src/FMData/FMData.csproj index efed8fa..4c0934c 100644 --- a/src/FMData/FMData.csproj +++ b/src/FMData/FMData.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard1.3;net45;net6.0 + netstandard2.0;netstandard1.3;net45;net6.0;net8.0 latest true FMData @@ -28,7 +28,7 @@ - 5.1 + 5.2 v beta.0 diff --git a/src/FMData/FieldMetadata.cs b/src/FMData/FieldMetadata.cs index ede640e..929756d 100644 --- a/src/FMData/FieldMetadata.cs +++ b/src/FMData/FieldMetadata.cs @@ -1,81 +1,86 @@ -namespace FMData +namespace FMData; + +/// +/// Field Metadata Instance +/// +public class FieldMetadata { /// - /// FIeld Metadata Instance + /// Field Name /// - public class FieldMetadata - { - /// - /// Field Name - /// - public string Name { get; set; } + public string Name { get; set; } - /// - /// Field Type. - /// - /// normal - public string Type { get; set; } + /// + /// Field Type. + /// + /// normal + public string Type { get; set; } - /// - /// Field Display Type. - /// - /// editText - public string DisplayType { get; set; } + /// + /// Field Display Type. + /// + /// editText + public string DisplayType { get; set; } - /// - /// Field Value List. - /// - public string ValueList { get; set; } + /// + /// The data type for the field. + /// + /// text + public string Result { get; set; } - /// - /// Is Field Global? - /// - public bool Global { get; set; } + /// + /// Field Value List. + /// + public string ValueList { get; set; } - /// - /// Is Field AutoEnter. - /// - public bool AutoEnter { get; set; } + /// + /// Is Field Global? + /// + public bool Global { get; set; } - /// - /// Is Field Four Digit Year? - /// - public bool FourDigitYear { get; set; } + /// + /// Is Field AutoEnter. + /// + public bool AutoEnter { get; set; } + + /// + /// Is Field Four Digit Year? + /// + public bool FourDigitYear { get; set; } - /// - /// Field Maximum Repeat. - /// - public int MaxRepeat { get; set; } + /// + /// Field Maximum Repeat. + /// + public int MaxRepeat { get; set; } - /// - /// Field Maximum Length (characters). - /// - public int MaxCharacters { get; set; } + /// + /// Field Maximum Length (characters). + /// + public int MaxCharacters { get; set; } - /// - /// Field NotEmpty - /// - public bool NotEmpty { get; set; } + /// + /// Field NotEmpty + /// + public bool NotEmpty { get; set; } - /// - /// Is Numeric - /// - public bool Numeric { get; set; } + /// + /// Is Numeric + /// + public bool Numeric { get; set; } - /// - /// Time Of day - /// - /// - public bool TimeOfDay { get; set; } + /// + /// Time Of day + /// + /// + public bool TimeOfDay { get; set; } - /// - /// Repetition Start - /// - public int RepetitionStart { get; set; } + /// + /// Repetition Start + /// + public int RepetitionStart { get; set; } - /// - /// Repetition End - /// - public int RepetitionEnd { get; set; } - } + /// + /// Repetition End + /// + public int RepetitionEnd { get; set; } } diff --git a/src/FMData/FileMakerApiClientBase.cs b/src/FMData/FileMakerApiClientBase.cs index 3b7e052..754acbb 100644 --- a/src/FMData/FileMakerApiClientBase.cs +++ b/src/FMData/FileMakerApiClientBase.cs @@ -267,7 +267,7 @@ public Task CreateAsync( /// /// The database to query. /// The names of the layouts in the specified database. - [Obsolete] + [Obsolete("Cannot call Metadata method on different file than open file. https://github.com/fuzzzerd/fmdata/issues/117")] public Task> GetLayoutsAsync(string database) { if (database != FileName) @@ -288,7 +288,7 @@ public Task> GetLayoutsAsync(string database /// /// The database to query. /// The names of the scripts in the specified database. - [Obsolete] + [Obsolete("Cannot call Metadata method on different file than open file. https://github.com/fuzzzerd/fmdata/issues/117")] public Task> GetScriptsAsync(string database) { if (database != FileName) @@ -311,7 +311,7 @@ public Task> GetScriptsAsync(string database /// The layout to get data about. /// Optional RecordId, for getting layout data specific to a record. ValueLists, etc. /// An instance of the LayoutMetadata class for the specified layout. - [Obsolete] + [Obsolete("Cannot call Metadata method on different file than open file. https://github.com/fuzzzerd/fmdata/issues/117")] public Task GetLayoutAsync(string database, string layout, int? recordId = null) { if (database != FileName) @@ -466,7 +466,7 @@ public Task> FindAsync( .SetOffset(skip); req.Script = script; req.ScriptParameter = scriptParameter; - return SendAsync(req, fmIdFunc, fmModIdFunc); + return SendAsync(req, fmIdFunc, fmModIdFunc); } /// @@ -628,6 +628,7 @@ public Task DeleteAsync( /// /// Send a Find Record request to the FileMaker API. /// + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] public abstract Task>> SendAsync(IFindRequest> req); /// @@ -641,7 +642,7 @@ public virtual Task> SendAsync( /// public virtual Task> SendAsync( IFindRequest req, - Func fmId) where T : class, new() => SendAsync(req, fmId, null); + Func fmId) where T : class, new() => SendAsync(req, fmId, null); /// /// Send a Find Record request to the FileMaker API. @@ -651,28 +652,30 @@ public virtual async Task> SendAsync( Func fmId, Func modId) where T : class, new() { - var (data, info) = await SendAsync(req, false, fmId, modId).ConfigureAwait(false); + var (data, _) = await SendFindRequestAsync(req, fmId, modId).ConfigureAwait(false); return data; } - /// - /// Send a Find Record request to the FileMaker API. - /// - public abstract Task<(IEnumerable, DataInfoModel)> SendAsync( + /// + public virtual async Task<(IEnumerable, DataInfoModel)> SendAsync( IFindRequest req, bool includeDataInfo, Func fmId = null, - Func modId = null) where T : class, new(); + Func modId = null) where T : class, new() + { + return await SendFindRequestAsync(req, fmId, modId).ConfigureAwait(false); + } + + /// + public abstract Task<(IEnumerable, DataInfoModel)> SendFindRequestAsync( + IFindRequest req, + Func fmId, + Func modId) where TResponse : class, new(); #endregion - /// - /// Find a record with utilizing a class instance to define the find request field values. - /// - /// The response type to extract and return. - /// The layout to perform the request on. - /// The dictionary of key/value pairs to find against. - /// + /// + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] public abstract Task> FindAsync(string layout, Dictionary req); /// diff --git a/src/FMData/IFileMakerApiClient.cs b/src/FMData/IFileMakerApiClient.cs index 5a8dc1b..8be30f5 100644 --- a/src/FMData/IFileMakerApiClient.cs +++ b/src/FMData/IFileMakerApiClient.cs @@ -20,7 +20,7 @@ public interface IFileMakerApiClient /// /// The initial find request data. /// The type used for the create request. - /// An IFindRequest{T} instance setup per the initial query paramater. + /// An IFindRequest{T} instance setup per the initial query parameter. ICreateRequest GenerateCreateRequest(T data); /// @@ -33,7 +33,7 @@ public interface IFileMakerApiClient /// /// The initial edit data request. /// The type used for the edit request. - /// An IEditRequest{T} instance setup per the initial query paramater. + /// An IEditRequest{T} instance setup per the initial query parameter. IEditRequest GenerateEditRequest(T data); /// @@ -46,7 +46,7 @@ public interface IFileMakerApiClient /// /// The initial find request data. /// The type used for the find request. - /// An IFindRequest{T} instance setup per the initial query paramater. + /// An IFindRequest{T} instance setup per the initial query parameter. IFindRequest GenerateFindRequest(T initialQuery); /// @@ -56,7 +56,7 @@ public interface IFileMakerApiClient #endregion /// - /// Runs a script with the specified layout context and with an optional (null/empty OK) paramater. + /// Runs a script with the specified layout context and with an optional (null/empty OK) parameter. /// /// The layout to use for the context of the script. /// The name of the script to run. @@ -166,7 +166,7 @@ public interface IFileMakerApiClient /// /// The database to query. /// The names of the layouts in the specified database. - [Obsolete] + [Obsolete("Cannot call Metadata method on different file than open file. https://github.com/fuzzzerd/fmdata/issues/117")] Task> GetLayoutsAsync(string database); /// @@ -182,7 +182,7 @@ public interface IFileMakerApiClient /// The layout to get data about. /// Optional RecordId, for getting layout data specific to a record. ValueLists, etc. /// An instance of the LayoutMetadata class for the specified layout. - [Obsolete] + [Obsolete("Cannot call Metadata method on different file than open file. https://github.com/fuzzzerd/fmdata/issues/117")] Task GetLayoutAsync(string database, string layout, int? recordId = null); /// @@ -198,7 +198,7 @@ public interface IFileMakerApiClient /// /// The database to query. /// The names of the scripts in the specified database. - [Obsolete] + [Obsolete("Cannot call Metadata method on different file than open file. https://github.com/fuzzzerd/fmdata/issues/117")] Task> GetScriptsAsync(string database); /// @@ -295,6 +295,7 @@ public interface IFileMakerApiClient /// /// /// + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] Task> FindAsync(string layout, Dictionary req); #endregion @@ -353,7 +354,7 @@ public interface IFileMakerApiClient /// /// Delete a record by FileMaker RecordId. /// - /// The filemaker RecordId to delete. + /// The FileMaker RecordId to delete. /// Used to pull the [TableAttribute] value to determine the layout to use. /// /// Use the other delete overload if the class does not use the [Table] attribute. @@ -443,6 +444,7 @@ Task UpdateContainerAsync( /// Find a record or records matching the request. /// /// Find request. + [Obsolete("Use SendFindRequestAsync() instead. See also: https://github.com/fuzzzerd/fmdata/pull/328")] Task>> SendAsync(IFindRequest> req); /// @@ -488,6 +490,22 @@ Task> SendAsync( Func fmId = null, Func modId = null) where T : class, new(); + /// + /// Find a record or records matching the request and include a data info model as well as the response. + /// + /// The Response type. + /// The Request type. + /// The find request parameters. + /// Function to assign the FileMaker RecordId to each instance of {T}. + /// Function to assign the FileMaker ModId to each instance of {T}. + /// An matching the request parameters. + /// The data info portion of the response is always returned when correctly parsed. + /// This method allows using separate Request and Response generics, which is useful when querying with dynamic input, but static output. + Task<(IEnumerable, DataInfoModel)> SendFindRequestAsync( + IFindRequest req, + Func fmId, + Func modId) where TResponse : class, new(); + /// /// Edit record. /// diff --git a/src/FMData/RestTargetVersion.cs b/src/FMData/RestTargetVersion.cs new file mode 100644 index 0000000..2ba0b9f --- /dev/null +++ b/src/FMData/RestTargetVersion.cs @@ -0,0 +1,21 @@ +namespace FMData +{ + /// + /// All supported FileMaker DataAPI endpoint versions. + /// + public enum RestTargetVersion + { + /// + /// Uses v1 endpoint of the DataAPI. + /// + v1, + /// + /// Uses v2 endpoint of the DataAPI. + /// + v2, + /// + /// Uses the latest endpoint version of the DataAPI. + /// + vLatest + } +} diff --git a/tests/FMData.Rest.Tests/DeleteRequestTests.cs b/tests/FMData.Rest.Tests/DeleteRequestTests.cs index ec4acec..95f5066 100644 --- a/tests/FMData.Rest.Tests/DeleteRequestTests.cs +++ b/tests/FMData.Rest.Tests/DeleteRequestTests.cs @@ -25,6 +25,9 @@ private static IFileMakerApiClient GetMockedClient() mockHttp.When(HttpMethod.Delete, $"{server}/fmi/data/v1/databases/{file}/layouts/{layout}/records/*") .Respond("application/json", DataApiResponses.SuccessfulDelete()); + // specifically return all non-matched with 404 not found. + mockHttp.Fallback.Respond(HttpStatusCode.NotFound); + var mockedClient = mockHttp.ToHttpClient(); var fdc = new FileMakerRestClient(mockedClient, new ConnectionInfo { FmsUri = server, Database = file, Username = user, Password = pass }); diff --git a/tests/FMData.Rest.Tests/FMData.Rest.Tests.csproj b/tests/FMData.Rest.Tests/FMData.Rest.Tests.csproj index 6b94c06..060e884 100644 --- a/tests/FMData.Rest.Tests/FMData.Rest.Tests.csproj +++ b/tests/FMData.Rest.Tests/FMData.Rest.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false @@ -46,11 +46,15 @@ - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs b/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs index ac0f1b5..88ee831 100644 --- a/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs +++ b/tests/FMData.Rest.Tests/Find.SendAsync.Tests.cs @@ -14,6 +14,7 @@ namespace FMData.Rest.Tests public class FindRequestTests { [Fact] + [Obsolete] public async Task SendAsync_Find_Should_ReturnData() { var fdc = FindTestsHelpers.GetMockedFDC(); @@ -36,6 +37,33 @@ public async Task SendAsync_EmptyFind_ShouldReturnMany() } [Fact] + public async Task SendAsync_Using_Var_With_Mappers_ShouldReturnMany() + { + var fdc = FindTestsHelpers.GetMockedFDC(); + Func fMRecordIdMapper = (o, id) => o.FileMakerRecordId = id; + Func fMModIdMapper = (o, id) => o.FileMakerRecordId = id; + + var response = await fdc.SendAsync(new FindRequest { Layout = "layout" }, fMRecordIdMapper, fMModIdMapper); + + Assert.Equal(2, response.Count()); + } + + [Fact] + public async Task SendAsync_Explicit_Type_With_Mappers_ShouldReturnMany() + { + var fdc = FindTestsHelpers.GetMockedFDC(); + Func fMRecordIdMapper = (o, id) => o.FileMakerRecordId = id; + Func fMModIdMapper = (o, id) => o.FileMakerRecordId = id; + + IEnumerable response; + + response = await fdc.SendAsync(new FindRequest { Layout = "layout" }, fMRecordIdMapper, fMModIdMapper); + + Assert.Equal(2, response.Count()); + } + + [Fact] + [Obsolete] public async Task SendAsync_FindWithoutQuery_ShouldConvertToGetRange_AndReturnMany() { var fdc = FindTestsHelpers.GetMockedFDC(); @@ -46,7 +74,8 @@ public async Task SendAsync_FindWithoutQuery_ShouldConvertToGetRange_AndReturnMa } [Fact] - public async Task SendAsync_FindWithoutlayout_ShouldThrowArgumentException() + [Obsolete] + public async Task SendAsync_FindWithoutLayout_ShouldThrowArgumentException() { var fdc = FindTestsHelpers.GetMockedFDC(); @@ -166,6 +195,7 @@ public async Task SendAsyncFind_WithoutLayout_ShouldThrow() } [Fact] + [Obsolete] public async Task SendAsync_Dictionary_WithPortals_ShouldHaveData() { // arrange @@ -223,6 +253,188 @@ public async Task SendAsync_Find_Should_Have_DataInfo() Assert.Equal(123, info.FoundCount); } + [Fact] + public async Task SendAsync_Find_Should_Have_DataInfo_FirstOverload() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + Func FMRecordIdMapper = (o, id) => o.FileMakerRecordId = id; + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new User() { Id = 35 }; + var req = new FindRequest() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendAsync(req, true, FMRecordIdMapper, null); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + + [Fact] + public async Task SendAsync_Find_Should_Have_DataInfo_SecondOverload() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + Func ModMap = (o, id) => o.FileMakerModId = id; + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new User() { Id = 35 }; + var req = new FindRequest() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendAsync(req, true, null, ModMap); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + + [Fact] + public async Task SendAsync_Using_Dictionary_Find_Should_Have_DataInfo() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new Dictionary() { { "Id", "35" } }; + var req = new FindRequest>() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendFindRequestAsync>(req, null, null); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + + [Fact] + public async Task SendAsync_Using_Dictionary_Find_Should_Have_DataInfo_OverloadOne() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + Func IdMap = (o, id) => o.FileMakerRecordId = id; + Func ModMap = (o, id) => o.FileMakerModId = id; + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new Dictionary() { { "Id", "35" } }; + var req = new FindRequest>() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendFindRequestAsync>(req, IdMap, null); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + + [Fact] + public async Task SendAsync_Using_Dictionary_Find_Should_Have_DataInfo_OverloadTwo() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + Func IdMap = (o, id) => o.FileMakerRecordId = id; + Func ModMap = (o, id) => o.FileMakerModId = id; + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new Dictionary() { { "Id", "35" } }; + var req = new FindRequest>() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendFindRequestAsync>(req, null, ModMap); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + + [Fact] + public async Task SendAsync_Using_Dictionary_Find_Should_Have_DataInfo_OverloadThree() + { + // arrange + var mockHttp = new MockHttpMessageHandler(); + Func IdMap = (o, id) => o.FileMakerRecordId = id; + Func ModMap = (o, id) => o.FileMakerModId = id; + + var layout = "the-layout"; + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Post, $"{FindTestsHelpers.Server}/fmi/data/v1/databases/{FindTestsHelpers.File}/layouts/{layout}/_find") + .Respond(HttpStatusCode.OK, "application/json", DataApiResponses.SuccessfulFindWithDataInfo()); + + var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), FindTestsHelpers.Connection); + + var toFind = new Dictionary() { { "Id", "35" } }; + var req = new FindRequest>() { Layout = layout }; + req.AddQuery(toFind, false); + + // act + var (data, info) = await fdc.SendFindRequestAsync>(req, IdMap, ModMap); + + // assert + Assert.NotEmpty(data); + Assert.Equal(1, info.ReturnedCount); + Assert.Equal(123, info.FoundCount); + } + [Fact] public async Task SendAsyncFind_WithOmit_Omits() { diff --git a/tests/FMData.Rest.Tests/GeneralTests.cs b/tests/FMData.Rest.Tests/GeneralTests.cs index 4134d2d..ffa243c 100644 --- a/tests/FMData.Rest.Tests/GeneralTests.cs +++ b/tests/FMData.Rest.Tests/GeneralTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; @@ -6,6 +7,7 @@ using System.Threading.Tasks; using FMData.Rest.Requests; using FMData.Rest.Tests.TestModels; +using Newtonsoft.Json.Linq; using RichardSzalay.MockHttp; using Xunit; @@ -197,5 +199,77 @@ public async Task Test_DateTime_To_Timestamp_Parsing() var responseDataContainsResult = response.Any(r => r.Created == DateTime.ParseExact("03/29/2018 15:22:09", "MM/dd/yyyy HH:mm:ss", CultureInfo.InvariantCulture)); Assert.True(responseDataContainsResult); } + + + [Fact] + public void Test_EndpointVersion_Default() + { + // arrange + var expected = "http://localhost/fmi/data/v1/databases/test-file/sessions"; + var testClients = CreateEndpointTestClients(null); + + // assert + foreach (var client in testClients) + { + Assert.Equal(expected, client.AuthEndpoint()); + } + } + + [Fact] + public void Test_EndpointVersion_v1() + { + // arrange + var expected = "http://localhost/fmi/data/v1/databases/test-file/sessions"; + var testClients = CreateEndpointTestClients(RestTargetVersion.v1); + + // assert + foreach (var client in testClients) + { + Assert.Equal(expected, client.AuthEndpoint()); + } + } + + [Fact] + public void Test_EndpointVersion_v2() + { + // arrange + var expected = "http://localhost/fmi/data/v2/databases/test-file/sessions"; + var testClients = CreateEndpointTestClients(RestTargetVersion.v2); + + // assert + foreach (var client in testClients) + { + Assert.Equal(expected, client.AuthEndpoint()); + } + } + + [Fact] + public void Test_EndpointVersion_vLatest() + { + // arrange + var expected = "http://localhost/fmi/data/vLatest/databases/test-file/sessions"; + var testClients = CreateEndpointTestClients(RestTargetVersion.vLatest); + + // assert + foreach (var client in testClients) + { + Assert.Equal(expected, client.AuthEndpoint()); + } + } + + private IEnumerable CreateEndpointTestClients(RestTargetVersion? targetVersion) + { + var mockHttp = new MockHttpMessageHandler(); + var server = "http://localhost"; + var file = "test-file"; + var user = "unit"; + var pass = "test"; + var connectionInfo = new ConnectionInfo { FmsUri = server, Database = file, Username = user, Password = pass, RestTargetVersion = targetVersion }; + + return [ + new(mockHttp.ToHttpClient(), connectionInfo), + new(mockHttp.ToHttpClient(), new DefaultAuthTokenProvider(connectionInfo)) + ]; + } } } diff --git a/tests/FMData.Xml.Tests/FMData.Xml.Tests.csproj b/tests/FMData.Xml.Tests/FMData.Xml.Tests.csproj index 0e4f19b..78fd8bc 100644 --- a/tests/FMData.Xml.Tests/FMData.Xml.Tests.csproj +++ b/tests/FMData.Xml.Tests/FMData.Xml.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false @@ -11,11 +11,15 @@ - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + +